mirror of
https://github.com/Feodor2/Mypal68.git
synced 2025-06-18 06:45:44 -04:00
Remove payments
This commit is contained in:
parent
1b44b5877f
commit
1d3013a6f8
@ -26,8 +26,6 @@ browser/branding/**/firefox-branding.js
|
||||
# Gzipped test file.
|
||||
browser/base/content/test/general/gZipOfflineChild.html
|
||||
browser/base/content/test/urlbar/file_blank_but_not_blank.html
|
||||
# Third-party code.
|
||||
browser/components/payments/res/vendor/*
|
||||
# Test files that are really json not js, and don't need to be linted.
|
||||
browser/components/sessionstore/test/unit/data/sessionstore_valid.js
|
||||
browser/components/sessionstore/test/unit/data/sessionstore_invalid.js
|
||||
|
@ -269,7 +269,6 @@ module.exports = {
|
||||
"dom/messagechannel/**",
|
||||
"dom/midi/**",
|
||||
"dom/network/**",
|
||||
"dom/payments/**",
|
||||
"dom/performance/**",
|
||||
"dom/permission/**",
|
||||
"dom/quota/**",
|
||||
|
@ -30,9 +30,6 @@ var gExceptionPaths = [
|
||||
"resource://gre/modules/commonjs/",
|
||||
"resource://gre/defaults/pref/",
|
||||
|
||||
// These resources are referenced using relative paths from html files.
|
||||
"resource://payments/",
|
||||
|
||||
// https://github.com/mozilla/activity-stream/issues/3053
|
||||
"resource://activity-stream/data/content/tippytop/images/",
|
||||
// https://github.com/mozilla/activity-stream/issues/3758
|
||||
|
@ -12,13 +12,6 @@ const kWhitelist = new Set([
|
||||
/browser\/content\/browser\/places\/controller.js$/,
|
||||
]);
|
||||
|
||||
const kESModuleList = new Set([
|
||||
/browser\/res\/payments\/(components|containers|mixins)\/.*\.js$/,
|
||||
/browser\/res\/payments\/paymentRequest\.js$/,
|
||||
/browser\/res\/payments\/PaymentsStore\.js$/,
|
||||
/browser\/aboutlogins\/components\/.*\.js$/,
|
||||
]);
|
||||
|
||||
// Normally we would use reflect.jsm to get Reflect.parse. However, if
|
||||
// we do that, then all the AST data is allocated in reflect.jsm's
|
||||
// zone. That exposes a bug in our GC. The GC collects reflect.jsm's
|
||||
|
@ -56,7 +56,6 @@ DIRS += ['build']
|
||||
if CONFIG['NIGHTLY_BUILD']:
|
||||
DIRS += [
|
||||
'aboutconfig',
|
||||
'payments',
|
||||
]
|
||||
|
||||
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
|
||||
|
@ -1,65 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
"res/components/*.js",
|
||||
"res/containers/*.js",
|
||||
"res/mixins/*.js",
|
||||
"res/paymentRequest.js",
|
||||
"res/PaymentsStore.js",
|
||||
"test/mochitest/test_*.html",
|
||||
],
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
},
|
||||
},
|
||||
{
|
||||
"files": "test/unit/head.js",
|
||||
"rules": {
|
||||
"no-unused-vars": ["error", {
|
||||
"args": "none",
|
||||
"vars": "local",
|
||||
}],
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
"mozilla/var-only-at-top-level": "error",
|
||||
|
||||
"block-scoped-var": "error",
|
||||
complexity: ["error", {
|
||||
max: 20,
|
||||
}],
|
||||
"max-nested-callbacks": ["error", 4],
|
||||
"no-console": ["error", { allow: ["error"] }],
|
||||
"no-fallthrough": "error",
|
||||
"no-multi-str": "error",
|
||||
"no-proto": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-unused-vars": ["error", {
|
||||
args: "none",
|
||||
vars: "all"
|
||||
}],
|
||||
"no-use-before-define": ["error", {
|
||||
functions: false,
|
||||
}],
|
||||
radix: "error",
|
||||
"valid-jsdoc": ["error", {
|
||||
prefer: {
|
||||
return: "returns",
|
||||
},
|
||||
preferType: {
|
||||
Boolean: "boolean",
|
||||
Number: "number",
|
||||
String: "string",
|
||||
bool: "boolean",
|
||||
},
|
||||
requireParamDescription: false,
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false,
|
||||
}],
|
||||
yoda: "error",
|
||||
},
|
||||
};
|
@ -1,352 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Singleton service acting as glue between the DOM APIs and the payment dialog UI.
|
||||
*
|
||||
* Communication from the DOM to the UI happens via the nsIPaymentUIService interface.
|
||||
* The UI talks to the DOM code via the nsIPaymentRequestService interface.
|
||||
* PaymentUIService is started by the DOM code lazily.
|
||||
*
|
||||
* For now the UI is shown in a native dialog but that is likely to change.
|
||||
* Tests should try to avoid relying on that implementation detail.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"BrowserWindowTracker",
|
||||
"resource:///modules/BrowserWindowTracker.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
this,
|
||||
"paymentSrv",
|
||||
"@mozilla.org/dom/payments/payment-request-service;1",
|
||||
"nsIPaymentRequestService"
|
||||
);
|
||||
|
||||
function PaymentUIService() {
|
||||
this.wrappedJSObject = this;
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let { ConsoleAPI } = ChromeUtils.import(
|
||||
"resource://gre/modules/Console.jsm"
|
||||
);
|
||||
return new ConsoleAPI({
|
||||
maxLogLevelPref: "dom.payments.loglevel",
|
||||
prefix: "Payment UI Service",
|
||||
});
|
||||
});
|
||||
this.log.debug("constructor");
|
||||
}
|
||||
|
||||
PaymentUIService.prototype = {
|
||||
classID: Components.ID("{01f8bd55-9017-438b-85ec-7c15d2b35cdc}"),
|
||||
QueryInterface: ChromeUtils.generateQI([Ci.nsIPaymentUIService]),
|
||||
|
||||
// nsIPaymentUIService implementation:
|
||||
|
||||
showPayment(requestId) {
|
||||
this.log.debug("showPayment:", requestId);
|
||||
let request = paymentSrv.getPaymentRequestById(requestId);
|
||||
let merchantBrowser = this.findBrowserByOuterWindowId(
|
||||
request.topOuterWindowId
|
||||
);
|
||||
let chromeWindow = merchantBrowser.ownerGlobal;
|
||||
let { gBrowser } = chromeWindow;
|
||||
let browserContainer = gBrowser.getBrowserContainer(merchantBrowser);
|
||||
let container = chromeWindow.document.createElementNS(XHTML_NS, "div");
|
||||
container.dataset.requestId = requestId;
|
||||
container.classList.add("paymentDialogContainer");
|
||||
container.hidden = true;
|
||||
let paymentsBrowser = this._createPaymentFrame(
|
||||
chromeWindow.document,
|
||||
requestId
|
||||
);
|
||||
|
||||
let pdwGlobal = {};
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://payments/content/paymentDialogWrapper.js",
|
||||
pdwGlobal
|
||||
);
|
||||
|
||||
paymentsBrowser.paymentDialogWrapper = pdwGlobal.paymentDialogWrapper;
|
||||
|
||||
// Create an <html:div> wrapper to absolutely position the <xul:browser>
|
||||
// because XUL elements don't support position:absolute.
|
||||
let absDiv = chromeWindow.document.createElementNS(XHTML_NS, "div");
|
||||
container.appendChild(absDiv);
|
||||
|
||||
// append the frame to start the loading
|
||||
absDiv.appendChild(paymentsBrowser);
|
||||
browserContainer.prepend(container);
|
||||
|
||||
// Initialize the wrapper once the <browser> is connected.
|
||||
paymentsBrowser.paymentDialogWrapper.init(requestId, paymentsBrowser);
|
||||
|
||||
this._attachBrowserEventListeners(merchantBrowser);
|
||||
|
||||
// Only show the frame and change the UI when the dialog is ready to show.
|
||||
paymentsBrowser.addEventListener(
|
||||
"tabmodaldialogready",
|
||||
function readyToShow() {
|
||||
if (!container) {
|
||||
// The dialog was closed by the DOM code before it was ready to be shown.
|
||||
return;
|
||||
}
|
||||
container.hidden = false;
|
||||
this._showDialog(merchantBrowser);
|
||||
}.bind(this),
|
||||
{
|
||||
once: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
abortPayment(requestId) {
|
||||
this.log.debug("abortPayment:", requestId);
|
||||
let abortResponse = Cc[
|
||||
"@mozilla.org/dom/payments/payment-abort-action-response;1"
|
||||
].createInstance(Ci.nsIPaymentAbortActionResponse);
|
||||
let found = this.closeDialog(requestId);
|
||||
|
||||
// if `win` is falsy, then we haven't found the dialog, so the abort fails
|
||||
// otherwise, the abort is successful
|
||||
let response = found
|
||||
? Ci.nsIPaymentActionResponse.ABORT_SUCCEEDED
|
||||
: Ci.nsIPaymentActionResponse.ABORT_FAILED;
|
||||
|
||||
abortResponse.init(requestId, response);
|
||||
paymentSrv.respondPayment(abortResponse);
|
||||
},
|
||||
|
||||
completePayment(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 paymentFrame;
|
||||
if (!closed) {
|
||||
// We need to call findDialog before we respond below as getPaymentRequestById
|
||||
// may fail due to the request being removed upon completion.
|
||||
paymentFrame = this.findDialog(requestId).paymentFrame;
|
||||
if (!paymentFrame) {
|
||||
this.log.error("completePayment: no dialog found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
paymentFrame.paymentDialogWrapper.updateRequest();
|
||||
}
|
||||
},
|
||||
|
||||
updatePayment(requestId) {
|
||||
let { paymentFrame } = this.findDialog(requestId);
|
||||
this.log.debug("updatePayment:", requestId);
|
||||
if (!paymentFrame) {
|
||||
this.log.error("updatePayment: no dialog found");
|
||||
return;
|
||||
}
|
||||
paymentFrame.paymentDialogWrapper.updateRequest();
|
||||
},
|
||||
|
||||
closePayment(requestId) {
|
||||
this.closeDialog(requestId);
|
||||
},
|
||||
|
||||
// other helper methods
|
||||
|
||||
_createPaymentFrame(doc, requestId) {
|
||||
let frame = doc.createXULElement("browser");
|
||||
frame.classList.add("paymentDialogContainerFrame");
|
||||
frame.setAttribute("type", "content");
|
||||
frame.setAttribute("remote", "true");
|
||||
frame.setAttribute("disablehistory", "true");
|
||||
frame.setAttribute("nodefaultsrc", "true");
|
||||
frame.setAttribute("transparent", "true");
|
||||
frame.setAttribute("selectmenulist", "ContentSelectDropdown");
|
||||
frame.setAttribute("autocompletepopup", "PopupAutoComplete");
|
||||
return frame;
|
||||
},
|
||||
|
||||
_attachBrowserEventListeners(merchantBrowser) {
|
||||
merchantBrowser.addEventListener("SwapDocShells", this);
|
||||
},
|
||||
|
||||
_showDialog(merchantBrowser) {
|
||||
let chromeWindow = merchantBrowser.ownerGlobal;
|
||||
// Prevent focusing or interacting with the <browser>.
|
||||
merchantBrowser.setAttribute("tabmodalPromptShowing", "true");
|
||||
|
||||
// Darken the merchant content area.
|
||||
let tabModalBackground = chromeWindow.document.createXULElement("box");
|
||||
tabModalBackground.classList.add(
|
||||
"tabModalBackground",
|
||||
"paymentDialogBackground"
|
||||
);
|
||||
// Insert the same way as <tabmodalprompt>.
|
||||
merchantBrowser.parentNode.insertBefore(
|
||||
tabModalBackground,
|
||||
merchantBrowser.nextElementSibling
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} requestId - Payment Request ID of the dialog to close.
|
||||
* @returns {boolean} whether the specified dialog was closed.
|
||||
*/
|
||||
closeDialog(requestId) {
|
||||
let { browser, dialogContainer, paymentFrame } = this.findDialog(requestId);
|
||||
if (!dialogContainer) {
|
||||
return false;
|
||||
}
|
||||
this.log.debug(`closing: ${requestId}`);
|
||||
paymentFrame.paymentDialogWrapper.uninit();
|
||||
dialogContainer.remove();
|
||||
browser.removeEventListener("SwapDocShells", this);
|
||||
|
||||
if (!dialogContainer.hidden) {
|
||||
// If the container is no longer hidden then the background was added after
|
||||
// `tabmodaldialogready` so remove it.
|
||||
browser.parentElement.querySelector(".paymentDialogBackground").remove();
|
||||
|
||||
if (
|
||||
!browser.tabModalPromptBox ||
|
||||
browser.tabModalPromptBox.listPrompts().length == 0
|
||||
) {
|
||||
browser.removeAttribute("tabmodalPromptShowing");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
getDialogContainerForMerchantBrowser(merchantBrowser) {
|
||||
return merchantBrowser.ownerGlobal.gBrowser
|
||||
.getBrowserContainer(merchantBrowser)
|
||||
.querySelector(".paymentDialogContainer");
|
||||
},
|
||||
|
||||
findDialog(requestId) {
|
||||
for (let win of BrowserWindowTracker.orderedWindows) {
|
||||
for (let dialogContainer of win.document.querySelectorAll(
|
||||
".paymentDialogContainer"
|
||||
)) {
|
||||
if (dialogContainer.dataset.requestId == requestId) {
|
||||
return {
|
||||
dialogContainer,
|
||||
paymentFrame: dialogContainer.querySelector(
|
||||
".paymentDialogContainerFrame"
|
||||
),
|
||||
browser: dialogContainer.parentElement.querySelector(
|
||||
".browserStack > browser"
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
|
||||
findBrowserByOuterWindowId(outerWindowId) {
|
||||
for (let win of BrowserWindowTracker.orderedWindows) {
|
||||
let browser = win.gBrowser.getBrowserForOuterWindowID(outerWindowId);
|
||||
if (!browser) {
|
||||
continue;
|
||||
}
|
||||
return browser;
|
||||
}
|
||||
|
||||
this.log.error(
|
||||
"findBrowserByOuterWindowId: No browser found for outerWindowId:",
|
||||
outerWindowId
|
||||
);
|
||||
return null;
|
||||
},
|
||||
|
||||
_moveDialogToNewBrowser(oldBrowser, newBrowser) {
|
||||
// Re-attach event listeners to the new browser.
|
||||
newBrowser.addEventListener("SwapDocShells", this);
|
||||
|
||||
let dialogContainer = this.getDialogContainerForMerchantBrowser(oldBrowser);
|
||||
let newBrowserContainer = newBrowser.ownerGlobal.gBrowser.getBrowserContainer(
|
||||
newBrowser
|
||||
);
|
||||
|
||||
// Clone the container tree
|
||||
let newDialogContainer = newBrowserContainer.ownerDocument.importNode(
|
||||
dialogContainer,
|
||||
true
|
||||
);
|
||||
|
||||
let oldFrame = dialogContainer.querySelector(
|
||||
".paymentDialogContainerFrame"
|
||||
);
|
||||
let newFrame = newDialogContainer.querySelector(
|
||||
".paymentDialogContainerFrame"
|
||||
);
|
||||
|
||||
// We need a document to be synchronously loaded in order to do the swap and
|
||||
// there's no point in wasting resources loading a dialog we're going to replace.
|
||||
newFrame.setAttribute("src", "about:blank");
|
||||
newFrame.setAttribute("nodefaultsrc", "true");
|
||||
|
||||
newBrowserContainer.prepend(newDialogContainer);
|
||||
|
||||
// Force the <browser> to be created so that it'll have a document loaded and frame created.
|
||||
// See `ourChildDocument` and `ourFrame` checks in nsFrameLoader::SwapWithOtherLoader.
|
||||
/* eslint-disable-next-line no-unused-expressions */
|
||||
newFrame.clientTop;
|
||||
|
||||
// Swap the frameLoaders to preserve the frame state
|
||||
newFrame.swapFrameLoaders(oldFrame);
|
||||
newFrame.paymentDialogWrapper = oldFrame.paymentDialogWrapper;
|
||||
newFrame.paymentDialogWrapper.changeAttachedFrame(newFrame);
|
||||
dialogContainer.remove();
|
||||
|
||||
this._showDialog(newBrowser);
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "SwapDocShells": {
|
||||
this._moveDialogToNewBrowser(event.target, event.detail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
var EXPORTED_SYMBOLS = ["PaymentUIService"];
|
@ -1,12 +0,0 @@
|
||||
# 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/.
|
||||
|
||||
Classes = [
|
||||
{
|
||||
'cid': '{01f8bd55-9017-438b-85ec-7c15d2b35cdc}',
|
||||
'contract_ids': ['@mozilla.org/dom/payments/payment-ui-service;1'],
|
||||
'jsm': 'resource:///modules/PaymentUIService.jsm',
|
||||
'constructor': 'PaymentUIService',
|
||||
},
|
||||
]
|
@ -1,181 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This frame script only exists to mediate communications between the
|
||||
* unprivileged frame in a content process and the privileged dialog wrapper
|
||||
* in the UI process on the main thread.
|
||||
*
|
||||
* `paymentChromeToContent` messages from the privileged wrapper are converted
|
||||
* into DOM events of the same name.
|
||||
* `paymentContentToChrome` custom DOM events from the unprivileged frame are
|
||||
* converted into messages of the same name.
|
||||
*
|
||||
* Business logic should stay out of this shim.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
/* eslint-env mozilla/frame-script */
|
||||
/* global Services */
|
||||
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"FormAutofill",
|
||||
"resource://formautofill/FormAutofill.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"FormAutofillUtils",
|
||||
"resource://formautofill/FormAutofillUtils.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"AppConstants",
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
|
||||
const SAVE_CREDITCARD_DEFAULT_PREF = "dom.payments.defaults.saveCreditCard";
|
||||
const SAVE_ADDRESS_DEFAULT_PREF = "dom.payments.defaults.saveAddress";
|
||||
|
||||
let PaymentFrameScript = {
|
||||
init() {
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let { ConsoleAPI } = ChromeUtils.import(
|
||||
"resource://gre/modules/Console.jsm"
|
||||
);
|
||||
return new ConsoleAPI({
|
||||
maxLogLevelPref: "dom.payments.loglevel",
|
||||
prefix: "paymentDialogFrameScript",
|
||||
});
|
||||
});
|
||||
|
||||
addEventListener("paymentContentToChrome", this, false, true);
|
||||
|
||||
addMessageListener("paymentChromeToContent", this);
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
this.sendToChrome(event);
|
||||
},
|
||||
|
||||
receiveMessage({ data: { messageType, data } }) {
|
||||
this.sendToContent(messageType, data);
|
||||
},
|
||||
|
||||
setupContentConsole() {
|
||||
let privilegedLogger = content.window.console.createInstance({
|
||||
maxLogLevelPref: "dom.payments.loglevel",
|
||||
prefix: "paymentDialogContent",
|
||||
});
|
||||
|
||||
let contentLogObject = Cu.waiveXrays(content).log;
|
||||
for (let name of ["error", "warn", "info", "debug"]) {
|
||||
Cu.exportFunction(
|
||||
privilegedLogger[name].bind(privilegedLogger),
|
||||
contentLogObject,
|
||||
{
|
||||
defineAs: name,
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Expose privileged utility functions to the unprivileged page.
|
||||
*/
|
||||
exposeUtilityFunctions() {
|
||||
let waivedContent = Cu.waiveXrays(content);
|
||||
let PaymentDialogUtils = {
|
||||
DEFAULT_REGION: FormAutofill.DEFAULT_REGION,
|
||||
countries: FormAutofill.countries,
|
||||
|
||||
getAddressLabel(address, addressFields = null) {
|
||||
return FormAutofillUtils.getAddressLabel(address, addressFields);
|
||||
},
|
||||
|
||||
getCreditCardNetworks() {
|
||||
let networks = FormAutofillUtils.getCreditCardNetworks();
|
||||
return Cu.cloneInto(networks, waivedContent);
|
||||
},
|
||||
|
||||
isCCNumber(value) {
|
||||
return FormAutofillUtils.isCCNumber(value);
|
||||
},
|
||||
|
||||
getFormFormat(country) {
|
||||
let format = FormAutofillUtils.getFormFormat(country);
|
||||
return Cu.cloneInto(format, waivedContent);
|
||||
},
|
||||
|
||||
findAddressSelectOption(selectEl, address, fieldName) {
|
||||
return FormAutofillUtils.findAddressSelectOption(
|
||||
selectEl,
|
||||
address,
|
||||
fieldName
|
||||
);
|
||||
},
|
||||
|
||||
getDefaultPreferences() {
|
||||
let prefValues = Cu.cloneInto(
|
||||
{
|
||||
saveCreditCardDefaultChecked: Services.prefs.getBoolPref(
|
||||
SAVE_CREDITCARD_DEFAULT_PREF,
|
||||
false
|
||||
),
|
||||
saveAddressDefaultChecked: Services.prefs.getBoolPref(
|
||||
SAVE_ADDRESS_DEFAULT_PREF,
|
||||
false
|
||||
),
|
||||
},
|
||||
waivedContent
|
||||
);
|
||||
return Cu.cloneInto(prefValues, waivedContent);
|
||||
},
|
||||
|
||||
isOfficialBranding() {
|
||||
return AppConstants.MOZILLA_OFFICIAL;
|
||||
},
|
||||
};
|
||||
waivedContent.PaymentDialogUtils = Cu.cloneInto(
|
||||
PaymentDialogUtils,
|
||||
waivedContent,
|
||||
{
|
||||
cloneFunctions: true,
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
sendToChrome({ detail }) {
|
||||
let { messageType } = detail;
|
||||
if (messageType == "initializeRequest") {
|
||||
this.setupContentConsole();
|
||||
this.exposeUtilityFunctions();
|
||||
}
|
||||
this.log.debug("sendToChrome:", messageType, detail);
|
||||
this.sendMessageToChrome(messageType, detail);
|
||||
},
|
||||
|
||||
sendToContent(messageType, detail = {}) {
|
||||
this.log.debug("sendToContent", messageType, detail);
|
||||
let response = Object.assign({ messageType }, detail);
|
||||
let event = new content.CustomEvent("paymentChromeToContent", {
|
||||
detail: Cu.cloneInto(response, content),
|
||||
});
|
||||
content.dispatchEvent(event);
|
||||
},
|
||||
|
||||
sendMessageToChrome(messageType, data = {}) {
|
||||
sendAsyncMessage(
|
||||
"paymentContentToChrome",
|
||||
Object.assign(data, { messageType })
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
PaymentFrameScript.init();
|
@ -1,926 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Runs in the privileged outer dialog. Each dialog loads this script in its
|
||||
* own scope.
|
||||
*/
|
||||
|
||||
/* exported paymentDialogWrapper */
|
||||
|
||||
"use strict";
|
||||
|
||||
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(Ci.nsIPaymentUIService);
|
||||
|
||||
const { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
const { XPCOMUtils } = ChromeUtils.import(
|
||||
"resource://gre/modules/XPCOMUtils.jsm"
|
||||
);
|
||||
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"BrowserWindowTracker",
|
||||
"resource:///modules/BrowserWindowTracker.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"FormAutofillUtils",
|
||||
"resource://formautofill/FormAutofillUtils.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"OSKeyStore",
|
||||
"resource://formautofill/OSKeyStore.jsm"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
this,
|
||||
"PrivateBrowsingUtils",
|
||||
"resource://gre/modules/PrivateBrowsingUtils.jsm"
|
||||
);
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "formAutofillStorage", () => {
|
||||
let storage;
|
||||
try {
|
||||
storage = ChromeUtils.import(
|
||||
"resource://formautofill/FormAutofillStorage.jsm",
|
||||
{}
|
||||
).formAutofillStorage;
|
||||
storage.initialize();
|
||||
} catch (ex) {
|
||||
storage = null;
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
|
||||
return storage;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "reauthPasswordPromptMessage", () => {
|
||||
const brandShortName = FormAutofillUtils.brandBundle.GetStringFromName(
|
||||
"brandShortName"
|
||||
);
|
||||
return FormAutofillUtils.stringBundle.formatStringFromName(
|
||||
`useCreditCardPasswordPrompt.${AppConstants.platform}`,
|
||||
[brandShortName]
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Temporary/transient storage for address and credit card records
|
||||
*
|
||||
* Implements a subset of the FormAutofillStorage collection class interface, and delegates to
|
||||
* those classes for some utility methods
|
||||
*/
|
||||
class TempCollection {
|
||||
constructor(type, data = {}) {
|
||||
/**
|
||||
* The name of the collection. e.g. 'addresses' or 'creditCards'
|
||||
* Used to access methods from the FormAutofillStorage collections
|
||||
*/
|
||||
this._type = type;
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
get _formAutofillCollection() {
|
||||
// lazy getter for the formAutofill collection - to resolve on first access
|
||||
Object.defineProperty(this, "_formAutofillCollection", {
|
||||
value: formAutofillStorage[this._type],
|
||||
writable: false,
|
||||
configurable: true,
|
||||
});
|
||||
return this._formAutofillCollection;
|
||||
}
|
||||
|
||||
get(guid) {
|
||||
return this._data[guid];
|
||||
}
|
||||
|
||||
async update(guid, record, preserveOldProperties) {
|
||||
let recordToSave = Object.assign(
|
||||
preserveOldProperties ? this._data[guid] : {},
|
||||
record
|
||||
);
|
||||
await this._formAutofillCollection.computeFields(recordToSave);
|
||||
return (this._data[guid] = recordToSave);
|
||||
}
|
||||
|
||||
async add(record) {
|
||||
let guid = "temp-" + Math.abs((Math.random() * 0xffffffff) | 0);
|
||||
let timeLastModified = Date.now();
|
||||
let recordToSave = Object.assign({ guid, timeLastModified }, record);
|
||||
await this._formAutofillCollection.computeFields(recordToSave);
|
||||
this._data[guid] = recordToSave;
|
||||
return guid;
|
||||
}
|
||||
|
||||
getAll() {
|
||||
return this._data;
|
||||
}
|
||||
}
|
||||
|
||||
var paymentDialogWrapper = {
|
||||
componentsLoaded: new Map(),
|
||||
frameWeakRef: null,
|
||||
mm: null,
|
||||
request: null,
|
||||
temporaryStore: null,
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference,
|
||||
]),
|
||||
|
||||
/**
|
||||
* @param {string} guid
|
||||
* @returns {object} containing only the requested payer values.
|
||||
*/
|
||||
async _convertProfileAddressToPayerData(guid) {
|
||||
let addressData =
|
||||
this.temporaryStore.addresses.get(guid) ||
|
||||
(await formAutofillStorage.addresses.get(guid));
|
||||
if (!addressData) {
|
||||
throw new Error(`Payer address not found: ${guid}`);
|
||||
}
|
||||
|
||||
let {
|
||||
requestPayerName,
|
||||
requestPayerEmail,
|
||||
requestPayerPhone,
|
||||
} = this.request.paymentOptions;
|
||||
|
||||
let payerData = {
|
||||
payerName: requestPayerName ? addressData.name : "",
|
||||
payerEmail: requestPayerEmail ? addressData.email : "",
|
||||
payerPhone: requestPayerPhone ? addressData.tel : "",
|
||||
};
|
||||
|
||||
return payerData;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} guid
|
||||
* @returns {nsIPaymentAddress}
|
||||
*/
|
||||
async _convertProfileAddressToPaymentAddress(guid) {
|
||||
let addressData =
|
||||
this.temporaryStore.addresses.get(guid) ||
|
||||
(await formAutofillStorage.addresses.get(guid));
|
||||
if (!addressData) {
|
||||
throw new Error(`Address not found: ${guid}`);
|
||||
}
|
||||
|
||||
let address = this.createPaymentAddress({
|
||||
addressLines: addressData["street-address"].split("\n"),
|
||||
city: addressData["address-level2"],
|
||||
country: addressData.country,
|
||||
dependentLocality: addressData["address-level3"],
|
||||
organization: addressData.organization,
|
||||
phone: addressData.tel,
|
||||
postalCode: addressData["postal-code"],
|
||||
recipient: addressData.name,
|
||||
region: addressData["address-level1"],
|
||||
// TODO (bug 1474905), The regionCode will be available when bug 1474905 is fixed
|
||||
// and the region text box is changed to a dropdown with the regionCode being the
|
||||
// value of the option and the region being the label for the option.
|
||||
// A regionCode should be either the empty string or one to three code points
|
||||
// that represent a region as the code element of an [ISO3166-2] country subdivision
|
||||
// name (i.e., the characters after the hyphen in an ISO3166-2 country subdivision
|
||||
// code element, such as "CA" for the state of California in the USA, or "11" for
|
||||
// the Lisbon district of Portugal).
|
||||
regionCode: "",
|
||||
});
|
||||
|
||||
return address;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} guid The GUID of the basic card record from storage.
|
||||
* @param {string} cardSecurityCode The associated card security code (CVV/CCV/etc.)
|
||||
* @throws If there is an error decrypting
|
||||
* @returns {nsIBasicCardResponseData?} returns response data or null (if the
|
||||
* master password dialog was cancelled);
|
||||
*/
|
||||
async _convertProfileBasicCardToPaymentMethodData(guid, cardSecurityCode) {
|
||||
let cardData =
|
||||
this.temporaryStore.creditCards.get(guid) ||
|
||||
(await formAutofillStorage.creditCards.get(guid));
|
||||
if (!cardData) {
|
||||
throw new Error(`Basic card not found in storage: ${guid}`);
|
||||
}
|
||||
|
||||
let cardNumber;
|
||||
try {
|
||||
cardNumber = await OSKeyStore.decrypt(
|
||||
cardData["cc-number-encrypted"],
|
||||
reauthPasswordPromptMessage
|
||||
);
|
||||
} catch (ex) {
|
||||
if (ex.result != Cr.NS_ERROR_ABORT) {
|
||||
throw ex;
|
||||
}
|
||||
// User canceled master password entry
|
||||
return null;
|
||||
}
|
||||
|
||||
let billingAddressGUID = cardData.billingAddressGUID;
|
||||
let billingAddress;
|
||||
try {
|
||||
billingAddress = await this._convertProfileAddressToPaymentAddress(
|
||||
billingAddressGUID
|
||||
);
|
||||
} catch (ex) {
|
||||
// The referenced address may not exist if it was deleted or hasn't yet synced to this profile
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
let methodData = this.createBasicCardResponseData({
|
||||
cardholderName: cardData["cc-name"],
|
||||
cardNumber,
|
||||
expiryMonth: cardData["cc-exp-month"].toString().padStart(2, "0"),
|
||||
expiryYear: cardData["cc-exp-year"].toString(),
|
||||
cardSecurityCode,
|
||||
billingAddress,
|
||||
});
|
||||
|
||||
return methodData;
|
||||
},
|
||||
|
||||
init(requestId, frame) {
|
||||
if (!requestId || typeof requestId != "string") {
|
||||
throw new Error("Invalid PaymentRequest ID");
|
||||
}
|
||||
|
||||
// The Request object returned by the Payment Service is live and
|
||||
// will automatically get updated if event.updateWith is used.
|
||||
this.request = paymentSrv.getPaymentRequestById(requestId);
|
||||
|
||||
if (!this.request) {
|
||||
throw new Error(`PaymentRequest not found: ${requestId}`);
|
||||
}
|
||||
|
||||
this._attachToFrame(frame);
|
||||
this.mm.loadFrameScript(
|
||||
"chrome://payments/content/paymentDialogFrameScript.js",
|
||||
true
|
||||
);
|
||||
// Until we have bug 1446164 and bug 1407418 we use form autofill's temporary
|
||||
// shim for data-localization* attributes.
|
||||
this.mm.loadFrameScript("chrome://formautofill/content/l10n.js", true);
|
||||
frame.setAttribute("src", "resource://payments/paymentRequest.xhtml");
|
||||
|
||||
this.temporaryStore = {
|
||||
addresses: new TempCollection("addresses"),
|
||||
creditCards: new TempCollection("creditCards"),
|
||||
};
|
||||
},
|
||||
|
||||
uninit() {
|
||||
try {
|
||||
Services.obs.removeObserver(this, "message-manager-close");
|
||||
Services.obs.removeObserver(this, "formautofill-storage-changed");
|
||||
} catch (ex) {
|
||||
// Observers may not have been added yet
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Code here will be re-run at various times, e.g. initial show and
|
||||
* when a tab is detached to a different window.
|
||||
*
|
||||
* Code that should only run once belongs in `init`.
|
||||
* Code to only run upon detaching should be in `changeAttachedFrame`.
|
||||
*
|
||||
* @param {Element} frame
|
||||
*/
|
||||
_attachToFrame(frame) {
|
||||
this.frameWeakRef = Cu.getWeakReference(frame);
|
||||
this.mm = frame.frameLoader.messageManager;
|
||||
this.mm.addMessageListener("paymentContentToChrome", this);
|
||||
Services.obs.addObserver(this, "message-manager-close", true);
|
||||
},
|
||||
|
||||
/**
|
||||
* Called only when a frame is changed from one to another.
|
||||
*
|
||||
* @param {Element} frame
|
||||
*/
|
||||
changeAttachedFrame(frame) {
|
||||
this.mm.removeMessageListener("paymentContentToChrome", this);
|
||||
this._attachToFrame(frame);
|
||||
// This isn't in `attachToFrame` because we only want to do it once we've sent records.
|
||||
Services.obs.addObserver(this, "formautofill-storage-changed", true);
|
||||
},
|
||||
|
||||
createShowResponse({
|
||||
acceptStatus,
|
||||
methodName = "",
|
||||
methodData = null,
|
||||
payerName = "",
|
||||
payerEmail = "",
|
||||
payerPhone = "",
|
||||
}) {
|
||||
let showResponse = this.createComponentInstance(
|
||||
Ci.nsIPaymentShowActionResponse
|
||||
);
|
||||
|
||||
showResponse.init(
|
||||
this.request.requestId,
|
||||
acceptStatus,
|
||||
methodName,
|
||||
methodData,
|
||||
payerName,
|
||||
payerEmail,
|
||||
payerPhone
|
||||
);
|
||||
return showResponse;
|
||||
},
|
||||
|
||||
createBasicCardResponseData({
|
||||
cardholderName = "",
|
||||
cardNumber,
|
||||
expiryMonth = "",
|
||||
expiryYear = "",
|
||||
cardSecurityCode = "",
|
||||
billingAddress = null,
|
||||
}) {
|
||||
const basicCardResponseData = Cc[
|
||||
"@mozilla.org/dom/payments/basiccard-response-data;1"
|
||||
].createInstance(Ci.nsIBasicCardResponseData);
|
||||
basicCardResponseData.initData(
|
||||
cardholderName,
|
||||
cardNumber,
|
||||
expiryMonth,
|
||||
expiryYear,
|
||||
cardSecurityCode,
|
||||
billingAddress
|
||||
);
|
||||
return basicCardResponseData;
|
||||
},
|
||||
|
||||
createPaymentAddress({
|
||||
addressLines = [],
|
||||
city = "",
|
||||
country = "",
|
||||
dependentLocality = "",
|
||||
organization = "",
|
||||
postalCode = "",
|
||||
phone = "",
|
||||
recipient = "",
|
||||
region = "",
|
||||
regionCode = "",
|
||||
sortingCode = "",
|
||||
}) {
|
||||
const paymentAddress = Cc[
|
||||
"@mozilla.org/dom/payments/payment-address;1"
|
||||
].createInstance(Ci.nsIPaymentAddress);
|
||||
const addressLine = Cc["@mozilla.org/array;1"].createInstance(
|
||||
Ci.nsIMutableArray
|
||||
);
|
||||
for (let line of addressLines) {
|
||||
const address = Cc["@mozilla.org/supports-string;1"].createInstance(
|
||||
Ci.nsISupportsString
|
||||
);
|
||||
address.data = line;
|
||||
addressLine.appendElement(address);
|
||||
}
|
||||
paymentAddress.init(
|
||||
country,
|
||||
addressLine,
|
||||
region,
|
||||
regionCode,
|
||||
city,
|
||||
dependentLocality,
|
||||
postalCode,
|
||||
sortingCode,
|
||||
organization,
|
||||
recipient,
|
||||
phone
|
||||
);
|
||||
return paymentAddress;
|
||||
},
|
||||
|
||||
createComponentInstance(componentInterface) {
|
||||
let componentName;
|
||||
switch (componentInterface) {
|
||||
case Ci.nsIPaymentShowActionResponse: {
|
||||
componentName =
|
||||
"@mozilla.org/dom/payments/payment-show-action-response;1";
|
||||
break;
|
||||
}
|
||||
case Ci.nsIGeneralResponseData: {
|
||||
componentName = "@mozilla.org/dom/payments/general-response-data;1";
|
||||
break;
|
||||
}
|
||||
}
|
||||
let component = this.componentsLoaded.get(componentName);
|
||||
|
||||
if (!component) {
|
||||
component = Cc[componentName];
|
||||
this.componentsLoaded.set(componentName, component);
|
||||
}
|
||||
|
||||
return component.createInstance(componentInterface);
|
||||
},
|
||||
|
||||
async fetchSavedAddresses() {
|
||||
let savedAddresses = {};
|
||||
for (let address of await formAutofillStorage.addresses.getAll()) {
|
||||
savedAddresses[address.guid] = address;
|
||||
}
|
||||
return savedAddresses;
|
||||
},
|
||||
|
||||
async fetchSavedPaymentCards() {
|
||||
let savedBasicCards = {};
|
||||
for (let card of await formAutofillStorage.creditCards.getAll()) {
|
||||
savedBasicCards[card.guid] = card;
|
||||
// Filter out the encrypted card number since the dialog content is
|
||||
// considered untrusted and runs in a content process.
|
||||
delete card["cc-number-encrypted"];
|
||||
|
||||
// ensure each card has a methodName property
|
||||
if (!card.methodName) {
|
||||
card.methodName = "basic-card";
|
||||
}
|
||||
}
|
||||
return savedBasicCards;
|
||||
},
|
||||
|
||||
fetchTempPaymentCards() {
|
||||
let creditCards = this.temporaryStore.creditCards.getAll();
|
||||
for (let card of Object.values(creditCards)) {
|
||||
// Ensure each card has a methodName property.
|
||||
if (!card.methodName) {
|
||||
card.methodName = "basic-card";
|
||||
}
|
||||
}
|
||||
return creditCards;
|
||||
},
|
||||
|
||||
async onAutofillStorageChange() {
|
||||
let [savedAddresses, savedBasicCards] = await Promise.all([
|
||||
this.fetchSavedAddresses(),
|
||||
this.fetchSavedPaymentCards(),
|
||||
]);
|
||||
|
||||
this.sendMessageToContent("updateState", {
|
||||
savedAddresses,
|
||||
savedBasicCards,
|
||||
});
|
||||
},
|
||||
|
||||
sendMessageToContent(messageType, data = {}) {
|
||||
this.mm.sendAsyncMessage("paymentChromeToContent", {
|
||||
data,
|
||||
messageType,
|
||||
});
|
||||
},
|
||||
|
||||
updateRequest() {
|
||||
// There is no need to update this.request since the object is live
|
||||
// and will automatically get updated if event.updateWith is used.
|
||||
let requestSerialized = this._serializeRequest(this.request);
|
||||
|
||||
this.sendMessageToContent("updateState", {
|
||||
request: requestSerialized,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Recursively convert and filter input to the subset of data types supported by JSON
|
||||
*
|
||||
* @param {*} value - any type of input to serialize
|
||||
* @param {string?} name - name or key associated with this input.
|
||||
* E.g. property name or array index.
|
||||
* @returns {*} serialized deep copy of the value
|
||||
*/
|
||||
_serializeRequest(value, name = null) {
|
||||
// Primitives: String, Number, Boolean, null
|
||||
let type = typeof value;
|
||||
if (
|
||||
value === null ||
|
||||
type == "string" ||
|
||||
type == "number" ||
|
||||
type == "boolean"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
if (name == "topLevelPrincipal") {
|
||||
// Manually serialize the nsIPrincipal.
|
||||
let displayHost = value.URI.displayHost;
|
||||
return {
|
||||
URI: {
|
||||
displayHost,
|
||||
},
|
||||
};
|
||||
}
|
||||
if (type == "function" || type == "undefined") {
|
||||
return undefined;
|
||||
}
|
||||
// Structures: nsIArray
|
||||
if (value instanceof Ci.nsIArray) {
|
||||
let iface;
|
||||
let items = [];
|
||||
switch (name) {
|
||||
case "displayItems": // falls through
|
||||
case "additionalDisplayItems":
|
||||
iface = Ci.nsIPaymentItem;
|
||||
break;
|
||||
case "shippingOptions":
|
||||
iface = Ci.nsIPaymentShippingOption;
|
||||
break;
|
||||
case "paymentMethods":
|
||||
iface = Ci.nsIPaymentMethodData;
|
||||
break;
|
||||
case "modifiers":
|
||||
iface = Ci.nsIPaymentDetailsModifier;
|
||||
break;
|
||||
}
|
||||
if (!iface) {
|
||||
throw new Error(
|
||||
`No interface associated with the members of the ${name} nsIArray`
|
||||
);
|
||||
}
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
let item = value.queryElementAt(i, iface);
|
||||
let result = this._serializeRequest(item, i);
|
||||
if (result !== undefined) {
|
||||
items.push(result);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
// Structures: Arrays
|
||||
if (Array.isArray(value)) {
|
||||
let items = value
|
||||
.map(item => this._serializeRequest(item))
|
||||
.filter(item => item !== undefined);
|
||||
return items;
|
||||
}
|
||||
// Structures: Objects
|
||||
let obj = {};
|
||||
for (let [key, item] of Object.entries(value)) {
|
||||
let result = this._serializeRequest(item, key);
|
||||
if (result !== undefined) {
|
||||
obj[key] = result;
|
||||
}
|
||||
}
|
||||
return obj;
|
||||
},
|
||||
|
||||
async initializeFrame() {
|
||||
// We don't do this earlier as it's only necessary once this function sends
|
||||
// the initial saved records.
|
||||
Services.obs.addObserver(this, "formautofill-storage-changed", true);
|
||||
|
||||
let requestSerialized = this._serializeRequest(this.request);
|
||||
let chromeWindow = this.frameWeakRef.get().ownerGlobal;
|
||||
let isPrivate = PrivateBrowsingUtils.isWindowPrivate(chromeWindow);
|
||||
|
||||
let [savedAddresses, savedBasicCards] = await Promise.all([
|
||||
this.fetchSavedAddresses(),
|
||||
this.fetchSavedPaymentCards(),
|
||||
]);
|
||||
|
||||
this.sendMessageToContent("showPaymentRequest", {
|
||||
request: requestSerialized,
|
||||
savedAddresses,
|
||||
tempAddresses: this.temporaryStore.addresses.getAll(),
|
||||
savedBasicCards,
|
||||
tempBasicCards: this.fetchTempPaymentCards(),
|
||||
isPrivate,
|
||||
});
|
||||
},
|
||||
|
||||
debugFrame() {
|
||||
// To avoid self-XSS-type attacks, ensure that Browser Chrome debugging is enabled.
|
||||
if (!Services.prefs.getBoolPref("devtools.chrome.enabled", false)) {
|
||||
Cu.reportError(
|
||||
"devtools.chrome.enabled must be enabled to debug the frame"
|
||||
);
|
||||
return;
|
||||
}
|
||||
let { gDevToolsBrowser } = ChromeUtils.import(
|
||||
"resource://devtools/client/framework/gDevTools.jsm"
|
||||
);
|
||||
gDevToolsBrowser.openContentProcessToolbox({
|
||||
selectedBrowser: this.frameWeakRef.get(),
|
||||
});
|
||||
},
|
||||
|
||||
onOpenPreferences() {
|
||||
BrowserWindowTracker.getTopWindow().openPreferences(
|
||||
"privacy-form-autofill"
|
||||
);
|
||||
},
|
||||
|
||||
onPaymentCancel() {
|
||||
const showResponse = this.createShowResponse({
|
||||
acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_REJECTED,
|
||||
});
|
||||
|
||||
paymentSrv.respondPayment(showResponse);
|
||||
paymentUISrv.closePayment(this.request.requestId);
|
||||
},
|
||||
|
||||
async onPay({
|
||||
selectedPayerAddressGUID: payerGUID,
|
||||
selectedPaymentCardGUID: paymentCardGUID,
|
||||
selectedPaymentCardSecurityCode: cardSecurityCode,
|
||||
selectedShippingAddressGUID: shippingGUID,
|
||||
}) {
|
||||
let methodData;
|
||||
try {
|
||||
methodData = await this._convertProfileBasicCardToPaymentMethodData(
|
||||
paymentCardGUID,
|
||||
cardSecurityCode
|
||||
);
|
||||
} catch (ex) {
|
||||
// TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user
|
||||
// to re-enter credit card # from management UI.
|
||||
Cu.reportError(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!methodData) {
|
||||
// TODO (Bug 1429265/Bug 1429205): Handle when a user hits cancel on the
|
||||
// Master Password dialog.
|
||||
Cu.reportError(
|
||||
"Bug 1429265/Bug 1429205: User canceled master password entry"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let payerName = "";
|
||||
let payerEmail = "";
|
||||
let payerPhone = "";
|
||||
if (payerGUID) {
|
||||
let payerData = await this._convertProfileAddressToPayerData(payerGUID);
|
||||
payerName = payerData.payerName;
|
||||
payerEmail = payerData.payerEmail;
|
||||
payerPhone = payerData.payerPhone;
|
||||
}
|
||||
|
||||
// Update the lastUsedTime for the payerAddress and paymentCard. Check if
|
||||
// the record exists in formAutofillStorage because it may be temporary.
|
||||
if (
|
||||
shippingGUID &&
|
||||
(await formAutofillStorage.addresses.get(shippingGUID))
|
||||
) {
|
||||
formAutofillStorage.addresses.notifyUsed(shippingGUID);
|
||||
}
|
||||
if (payerGUID && (await formAutofillStorage.addresses.get(payerGUID))) {
|
||||
formAutofillStorage.addresses.notifyUsed(payerGUID);
|
||||
}
|
||||
if (await formAutofillStorage.creditCards.get(paymentCardGUID)) {
|
||||
formAutofillStorage.creditCards.notifyUsed(paymentCardGUID);
|
||||
}
|
||||
|
||||
this.pay({
|
||||
methodName: "basic-card",
|
||||
methodData,
|
||||
payerName,
|
||||
payerEmail,
|
||||
payerPhone,
|
||||
});
|
||||
},
|
||||
|
||||
pay({ payerName, payerEmail, payerPhone, methodName, methodData }) {
|
||||
const showResponse = this.createShowResponse({
|
||||
acceptStatus: Ci.nsIPaymentActionResponse.PAYMENT_ACCEPTED,
|
||||
payerName,
|
||||
payerEmail,
|
||||
payerPhone,
|
||||
methodName,
|
||||
methodData,
|
||||
});
|
||||
paymentSrv.respondPayment(showResponse);
|
||||
this.sendMessageToContent("responseSent");
|
||||
},
|
||||
|
||||
async onChangePayerAddress({ payerAddressGUID }) {
|
||||
if (payerAddressGUID) {
|
||||
// If a payer address was de-selected e.g. the selected address was deleted, we'll
|
||||
// just wait to send the address change when the payer address is eventually selected
|
||||
// before clicking Pay since it's a required field.
|
||||
let {
|
||||
payerName,
|
||||
payerEmail,
|
||||
payerPhone,
|
||||
} = await this._convertProfileAddressToPayerData(payerAddressGUID);
|
||||
paymentSrv.changePayerDetail(
|
||||
this.request.requestId,
|
||||
payerName,
|
||||
payerEmail,
|
||||
payerPhone
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async onChangePaymentMethod({
|
||||
selectedPaymentCardBillingAddressGUID: billingAddressGUID,
|
||||
}) {
|
||||
const methodName = "basic-card";
|
||||
let methodDetails;
|
||||
try {
|
||||
let billingAddress = await this._convertProfileAddressToPaymentAddress(
|
||||
billingAddressGUID
|
||||
);
|
||||
const basicCardChangeDetails = Cc[
|
||||
"@mozilla.org/dom/payments/basiccard-change-details;1"
|
||||
].createInstance(Ci.nsIBasicCardChangeDetails);
|
||||
basicCardChangeDetails.initData(billingAddress);
|
||||
methodDetails = basicCardChangeDetails.QueryInterface(
|
||||
Ci.nsIMethodChangeDetails
|
||||
);
|
||||
} catch (ex) {
|
||||
// TODO (Bug 1498403): Some kind of "credit card storage error" here, perhaps asking user
|
||||
// to re-enter credit card # from management UI.
|
||||
Cu.reportError(ex);
|
||||
return;
|
||||
}
|
||||
|
||||
paymentSrv.changePaymentMethod(
|
||||
this.request.requestId,
|
||||
methodName,
|
||||
methodDetails
|
||||
);
|
||||
},
|
||||
|
||||
async onChangeShippingAddress({ shippingAddressGUID }) {
|
||||
if (shippingAddressGUID) {
|
||||
// If a shipping address was de-selected e.g. the selected address was deleted, we'll
|
||||
// just wait to send the address change when the shipping address is eventually selected
|
||||
// before clicking Pay since it's a required field.
|
||||
let address = await this._convertProfileAddressToPaymentAddress(
|
||||
shippingAddressGUID
|
||||
);
|
||||
paymentSrv.changeShippingAddress(this.request.requestId, address);
|
||||
}
|
||||
},
|
||||
|
||||
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
|
||||
paymentUISrv.closePayment(this.request.requestId);
|
||||
},
|
||||
|
||||
async onUpdateAutofillRecord(collectionName, record, guid, messageID) {
|
||||
let responseMessage = {
|
||||
guid,
|
||||
messageID,
|
||||
stateChange: {},
|
||||
};
|
||||
try {
|
||||
let isTemporary = record.isTemporary;
|
||||
let collection = isTemporary
|
||||
? this.temporaryStore[collectionName]
|
||||
: formAutofillStorage[collectionName];
|
||||
|
||||
if (guid) {
|
||||
// We want to preserve old properties since the edit forms are often
|
||||
// shown without all fields visible/enabled and we don't want those
|
||||
// fields to be blanked upon saving. Examples of hidden/disabled fields:
|
||||
// email, cc-number, mailing-address on the payer forms, and payer fields
|
||||
// not requested in the payer form.
|
||||
let preserveOldProperties = true;
|
||||
await collection.update(guid, record, preserveOldProperties);
|
||||
} else {
|
||||
responseMessage.guid = await collection.add(record);
|
||||
}
|
||||
|
||||
if (isTemporary && collectionName == "addresses") {
|
||||
// there will be no formautofill-storage-changed event to update state
|
||||
// so add updated collection here
|
||||
Object.assign(responseMessage.stateChange, {
|
||||
tempAddresses: this.temporaryStore.addresses.getAll(),
|
||||
});
|
||||
}
|
||||
if (isTemporary && collectionName == "creditCards") {
|
||||
// there will be no formautofill-storage-changed event to update state
|
||||
// so add updated collection here
|
||||
Object.assign(responseMessage.stateChange, {
|
||||
tempBasicCards: this.fetchTempPaymentCards(),
|
||||
});
|
||||
}
|
||||
} catch (ex) {
|
||||
responseMessage.error = true;
|
||||
Cu.reportError(ex);
|
||||
} finally {
|
||||
this.sendMessageToContent(
|
||||
"updateAutofillRecord:Response",
|
||||
responseMessage
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @implements {nsIObserver}
|
||||
* @param {nsISupports} subject
|
||||
* @param {string} topic
|
||||
* @param {string} data
|
||||
*/
|
||||
observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "formautofill-storage-changed": {
|
||||
if (data == "notifyUsed") {
|
||||
break;
|
||||
}
|
||||
this.onAutofillStorageChange();
|
||||
break;
|
||||
}
|
||||
case "message-manager-close": {
|
||||
if (this.mm && subject == this.mm) {
|
||||
// Remove the observer to avoid message manager errors while the dialog
|
||||
// is closing and tests are cleaning up autofill storage.
|
||||
Services.obs.removeObserver(this, "formautofill-storage-changed");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage({ data }) {
|
||||
let { messageType } = data;
|
||||
|
||||
switch (messageType) {
|
||||
case "debugFrame": {
|
||||
this.debugFrame();
|
||||
break;
|
||||
}
|
||||
case "initializeRequest": {
|
||||
this.initializeFrame();
|
||||
break;
|
||||
}
|
||||
case "changePayerAddress": {
|
||||
this.onChangePayerAddress(data);
|
||||
break;
|
||||
}
|
||||
case "changePaymentMethod": {
|
||||
this.onChangePaymentMethod(data);
|
||||
break;
|
||||
}
|
||||
case "changeShippingAddress": {
|
||||
this.onChangeShippingAddress(data);
|
||||
break;
|
||||
}
|
||||
case "changeShippingOption": {
|
||||
this.onChangeShippingOption(data);
|
||||
break;
|
||||
}
|
||||
case "closeDialog": {
|
||||
this.onCloseDialogMessage();
|
||||
break;
|
||||
}
|
||||
case "openPreferences": {
|
||||
this.onOpenPreferences();
|
||||
break;
|
||||
}
|
||||
case "paymentCancel": {
|
||||
this.onPaymentCancel();
|
||||
break;
|
||||
}
|
||||
case "paymentDialogReady": {
|
||||
this.frameWeakRef.get().dispatchEvent(
|
||||
new Event("tabmodaldialogready", {
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
case "pay": {
|
||||
this.onPay(data);
|
||||
break;
|
||||
}
|
||||
case "updateAutofillRecord": {
|
||||
this.onUpdateAutofillRecord(
|
||||
data.collectionName,
|
||||
data.record,
|
||||
data.guid,
|
||||
data.messageID
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`paymentDialogWrapper: Unexpected messageType: ${messageType}`
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
@ -1,110 +0,0 @@
|
||||
==============
|
||||
WebPayments UI
|
||||
==============
|
||||
|
||||
User Interface for the WebPayments `Payment Request API <https://w3c.github.io/browser-payment-api/>`_ and `Payment Handler API <https://w3c.github.io/payment-handler/>`_.
|
||||
|
||||
|
||||
`Project Wiki <https://wiki.mozilla.org/Firefox/Features/Web_Payments>`_ |
|
||||
`#payments on IRC <ircs://irc.mozilla.org:6697/payments>`_ |
|
||||
`File a bug <https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=WebPayments%20UI&status_whiteboard=[webpayments]%20[triage]>`_
|
||||
|
||||
JSDoc style comments are used within the JS files of the component. This document will focus on higher-level and shared concepts.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 5
|
||||
|
||||
|
||||
Debugging/Development
|
||||
=====================
|
||||
|
||||
Relevant preferences: ``dom.payments.*``
|
||||
|
||||
Must Have Electrolysis
|
||||
----------------------
|
||||
|
||||
Web Payments `does not work without e10s <https://bugzilla.mozilla.org/show_bug.cgi?id=1365964>`_!
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
Set the pref ``dom.payments.loglevel`` to "Debug" to increase the verbosity of console messages.
|
||||
|
||||
Unprivileged UI Development
|
||||
---------------------------
|
||||
During development of the unprivileged custom elements, you can load the dialog in a tab with
|
||||
the url `resource://payments/paymentRequest.xhtml`.
|
||||
You can then use the debugging console to load sample data. Autofill add/edit form strings
|
||||
will not appear when developing this way until they are converted to FTL.
|
||||
You can force localization of Form Autofill strings using the following in the Browser Console when
|
||||
the `paymentRequest.xhtml` tab is selected then reloading::
|
||||
|
||||
gBrowser.selectedBrowser.messageManager.loadFrameScript("chrome://formautofill/content/l10n.js", true)
|
||||
|
||||
|
||||
Debugging Console
|
||||
-----------------
|
||||
|
||||
To open the debugging console in the dialog, use the keyboard shortcut
|
||||
**Ctrl-Alt-d (Ctrl-Option-d on macOS)**. While loading `paymentRequest.xhtml` directly in the
|
||||
browser, add `?debug=1` to have the debugging console open by default.
|
||||
|
||||
Debugging the unprivileged frame with the developer tools
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To open a debugger in the context of the remote payment frame, click the "Debug frame" button in the
|
||||
debugging console.
|
||||
|
||||
Use the ``tabs`` variable in the Browser Content Toolbox's console to access the frame contents.
|
||||
There can be multiple frames loaded in the same process so you will need to find the correct tab
|
||||
in the array by checking the file name is `paymentRequest.xhtml` (e.g. ``tabs[0].content.location``).
|
||||
|
||||
|
||||
Dialog Architecture
|
||||
===================
|
||||
|
||||
Privileged wrapper XUL document (paymentDialogWrapper.xul) containing a remote ``<xul:browser="true" remote="true">`` containing unprivileged XHTML (paymentRequest.xhtml).
|
||||
Keeping the dialog contents unprivileged is useful since the dialog will render payment line items and shipping options that are provided by web developers and should therefore be considered untrusted.
|
||||
In order to communicate across the process boundary a privileged frame script (`paymentDialogFrameScript.js`) is loaded into the iframe to relay messages.
|
||||
This is because the unprivileged document cannot access message managers.
|
||||
Instead, all communication across the privileged/unprivileged boundary is done via custom DOM events:
|
||||
|
||||
* A ``paymentContentToChrome`` event is dispatched when the dialog contents want to communicate with the privileged dialog wrapper.
|
||||
* A ``paymentChromeToContent`` event is dispatched on the ``window`` with the ``detail`` property populated when the privileged dialog wrapper communicates with the unprivileged dialog.
|
||||
|
||||
These events are converted to/from message manager messages of the same name to communicate to the other process.
|
||||
The purpose of `paymentDialogFrameScript.js` is to simply convert unprivileged DOM events to/from messages from the other process.
|
||||
|
||||
The dialog depends on the add/edit forms and storage from :doc:`Form Autofill </browser/extensions/formautofill/docs/index>` for addresses and credit cards.
|
||||
|
||||
Communication with the DOM
|
||||
--------------------------
|
||||
|
||||
Communication from the DOM to the UI happens via the `paymentUIService.js` (implementing ``nsIPaymentUIService``).
|
||||
The UI talks to the DOM code via the ``nsIPaymentRequestService`` interface.
|
||||
|
||||
|
||||
Custom Elements
|
||||
---------------
|
||||
|
||||
The Payment Request UI uses `Custom Elements <https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements>`_ for the UI components.
|
||||
|
||||
Some guidelines:
|
||||
|
||||
* There are some `mixins <https://dxr.mozilla.org/mozilla-central/source/browser/components/payments/res/mixins/>`_
|
||||
to provide commonly needed functionality to a custom element.
|
||||
* `res/containers/ <https://dxr.mozilla.org/mozilla-central/source/browser/components/payments/res/containers/>`_
|
||||
contains elements that react to application state changes,
|
||||
`res/components/ <https://dxr.mozilla.org/mozilla-central/source/browser/components/payments/res/components>`_
|
||||
contains elements that aren't connected to the state directly.
|
||||
* Elements should avoid having their own internal/private state and should react to state changes.
|
||||
Containers primarily use the application state (``requestStore``) while components primarily use attributes.
|
||||
* If you're overriding a lifecycle callback, don't forget to call that method on
|
||||
``super`` from the implementation to ensure that mixins and ancestor classes
|
||||
work properly.
|
||||
* From within a custom element, don't use ``document.getElementById`` or
|
||||
``document.querySelector*`` because they can return elements that are outside
|
||||
of the component, thus breaking the modularization. It can also cause problems
|
||||
if the elements you're looking for aren't attached to the document yet. Use
|
||||
``querySelector*`` on ``this`` (the custom element) or one of its descendants
|
||||
instead.
|
@ -1,26 +0,0 @@
|
||||
# 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/.
|
||||
|
||||
browser.jar:
|
||||
% content payments %content/payments/
|
||||
content/payments/paymentDialogFrameScript.js (content/paymentDialogFrameScript.js)
|
||||
content/payments/paymentDialogWrapper.js (content/paymentDialogWrapper.js)
|
||||
|
||||
% resource payments %res/payments/
|
||||
res/payments (res/paymentRequest.*)
|
||||
res/payments/components/ (res/components/*.css)
|
||||
res/payments/components/ (res/components/*.js)
|
||||
res/payments/components/ (res/components/*.svg)
|
||||
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)
|
||||
res/payments/PaymentsStore.js (res/PaymentsStore.js)
|
@ -1,34 +0,0 @@
|
||||
# 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/.
|
||||
|
||||
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'WebPayments UI')
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
'PaymentUIService.jsm',
|
||||
]
|
||||
|
||||
XPCOM_MANIFESTS += [
|
||||
'components.conf',
|
||||
]
|
||||
|
||||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
MOCHITEST_MANIFESTS += [
|
||||
'test/mochitest/formautofill/mochitest.ini',
|
||||
'test/mochitest/mochitest.ini',
|
||||
]
|
||||
|
||||
SPHINX_TREES['docs'] = 'docs'
|
||||
|
||||
with Files('docs/**'):
|
||||
SCHEDULES.exclusive = ['docs']
|
||||
|
||||
TESTING_JS_MODULES += [
|
||||
'test/PaymentTestUtils.jsm',
|
||||
]
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']
|
@ -1,97 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* The PaymentsStore class provides lightweight storage with an async publish/subscribe mechanism.
|
||||
* Synchronous state changes are batched to improve application performance and to reduce partial
|
||||
* state propagation.
|
||||
*/
|
||||
|
||||
export default class PaymentsStore {
|
||||
/**
|
||||
* @param {object} [defaultState = {}] The initial state of the store.
|
||||
*/
|
||||
constructor(defaultState = {}) {
|
||||
this._defaultState = Object.assign({}, defaultState);
|
||||
this._state = defaultState;
|
||||
this._nextNotifification = 0;
|
||||
this._subscribers = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current state as a shallow clone with a shallow freeze.
|
||||
* You shouldn't modify any part of the returned state object as that would bypass notifying
|
||||
* subscribers and could lead to subscribers assuming old state.
|
||||
*
|
||||
* @returns {Object} containing the current state
|
||||
*/
|
||||
getState() {
|
||||
return Object.freeze(Object.assign({}, this._state));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for testing to reset to the default state from the constructor.
|
||||
* @returns {Promise} returned by setState.
|
||||
*/
|
||||
async reset() {
|
||||
return this.setState(this._defaultState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment the current state with the keys of `obj` and asynchronously notify
|
||||
* state subscribers. As a result, multiple synchronous state changes will lead
|
||||
* to a single subscriber notification which leads to better performance and
|
||||
* reduces partial state changes.
|
||||
*
|
||||
* @param {Object} obj The object to augment the state with. Keys in the object
|
||||
* will be shallow copied with Object.assign.
|
||||
*
|
||||
* @example If the state is currently {a:3} then setState({b:"abc"}) will result in a state of
|
||||
* {a:3, b:"abc"}.
|
||||
*/
|
||||
async setState(obj) {
|
||||
Object.assign(this._state, obj);
|
||||
let thisChangeNum = ++this._nextNotifification;
|
||||
|
||||
// Let any synchronous setState calls that happen after the current setState call
|
||||
// complete first.
|
||||
// Their effects on the state will be batched up before the callback is actually called below.
|
||||
await Promise.resolve();
|
||||
|
||||
// Don't notify for state changes that are no longer the most recent. We only want to call the
|
||||
// callback once with the latest state.
|
||||
if (thisChangeNum !== this._nextNotifification) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let subscriber of this._subscribers) {
|
||||
try {
|
||||
subscriber.stateChangeCallback(this.getState());
|
||||
} catch (ex) {
|
||||
console.error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe the object to state changes notifications via a `stateChangeCallback` method.
|
||||
*
|
||||
* @param {Object} component to receive state change callbacks via a `stateChangeCallback` method.
|
||||
* If the component is already subscribed, do nothing.
|
||||
*/
|
||||
subscribe(component) {
|
||||
if (this._subscribers.has(component)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribers.add(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} component to stop receiving state change callbacks.
|
||||
*/
|
||||
unsubscribe(component) {
|
||||
this._subscribers.delete(component);
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
accepted-cards {
|
||||
margin: 1em 0;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: first baseline;
|
||||
}
|
||||
|
||||
.accepted-cards-label {
|
||||
display: inline-block;
|
||||
font-size: smaller;
|
||||
flex: 0 2 content;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.accepted-cards-list {
|
||||
display: inline-block;
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
flex: 2 1 auto;
|
||||
}
|
||||
|
||||
.accepted-cards-list > .accepted-cards-item {
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
margin-inline-start: 10px;
|
||||
vertical-align: middle;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
/* placeholders for specific card icons we don't yet have assets for */
|
||||
accepted-cards:not(.branded) .accepted-cards-item[data-network-id] {
|
||||
width: 48px;
|
||||
text-align: center;
|
||||
background-image: url("./card-icon.svg");
|
||||
-moz-context-properties: fill-opacity;
|
||||
fill-opacity: 0.5;
|
||||
}
|
||||
accepted-cards:not(.branded) .accepted-cards-item[data-network-id]::after {
|
||||
box-sizing: border-box;
|
||||
content: attr(data-network-id);
|
||||
padding: 8px 4px 0 4px;
|
||||
text-align: center;
|
||||
font-size: 0.7rem;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
We use .png / @2x.png images where we don't yet have a vector version of a logo
|
||||
*/
|
||||
.accepted-cards-item[data-network-id="amex"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-amex.png");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="cartebancaire"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-cartebancaire.png");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="diners"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-diners.svg");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="discover"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-discover.png");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="jcb"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-jcb.svg");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="mastercard"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-mastercard.svg");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="mir"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-mir.svg");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="unionpay"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-unionpay.svg");
|
||||
}
|
||||
|
||||
.accepted-cards-item[data-network-id="visa"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-visa.svg");
|
||||
}
|
||||
|
||||
@media (min-resolution: 1.1dppx) {
|
||||
.accepted-cards-item[data-network-id="amex"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-amex@2x.png");
|
||||
}
|
||||
.accepted-cards-item[data-network-id="cartebancaire"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-cartebancaire@2x.png");
|
||||
}
|
||||
.accepted-cards-item[data-network-id="discover"] {
|
||||
background-image: url("chrome://formautofill/content/third-party/cc-logo-discover@2x.png");
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
/* 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 PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
|
||||
/* import-globals-from ../unprivileged-fallbacks.js */
|
||||
|
||||
/**
|
||||
* <accepted-cards></accepted-cards>
|
||||
*/
|
||||
|
||||
export default class AcceptedCards extends PaymentStateSubscriberMixin(
|
||||
HTMLElement
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._listEl = document.createElement("ul");
|
||||
this._listEl.classList.add("accepted-cards-list");
|
||||
this._labelEl = document.createElement("span");
|
||||
this._labelEl.classList.add("accepted-cards-label");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.label = this.getAttribute("label");
|
||||
this.appendChild(this._labelEl);
|
||||
|
||||
this._listEl.textContent = "";
|
||||
let allNetworks = PaymentDialogUtils.getCreditCardNetworks();
|
||||
for (let network of allNetworks) {
|
||||
let item = document.createElement("li");
|
||||
item.classList.add("accepted-cards-item");
|
||||
item.dataset.networkId = network;
|
||||
item.setAttribute("aria-role", "image");
|
||||
item.setAttribute("aria-label", network);
|
||||
this._listEl.appendChild(item);
|
||||
}
|
||||
let isBranded = PaymentDialogUtils.isOfficialBranding();
|
||||
this.classList.toggle("branded", isBranded);
|
||||
this.appendChild(this._listEl);
|
||||
// Only call the connected super callback(s) once our markup is fully
|
||||
// connected
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
render(state) {
|
||||
let basicCardMethod = state.request.paymentMethods.find(
|
||||
method => method.supportedMethods == "basic-card"
|
||||
);
|
||||
let merchantNetworks =
|
||||
basicCardMethod &&
|
||||
basicCardMethod.data &&
|
||||
basicCardMethod.data.supportedNetworks;
|
||||
if (merchantNetworks && merchantNetworks.length) {
|
||||
for (let item of this._listEl.children) {
|
||||
let network = item.dataset.networkId;
|
||||
item.hidden = !(network && merchantNetworks.includes(network));
|
||||
}
|
||||
this.hidden = false;
|
||||
} else {
|
||||
// hide the whole list if the merchant didn't specify a preference
|
||||
this.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
set label(value) {
|
||||
this._labelEl.textContent = value;
|
||||
}
|
||||
|
||||
get acceptedItems() {
|
||||
return Array.from(this._listEl.children).filter(item => !item.hidden);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("accepted-cards", AcceptedCards);
|
@ -1,29 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
address-option.rich-option {
|
||||
grid-row-gap: 5px;
|
||||
}
|
||||
|
||||
address-option > .line {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
address-option > .line:empty {
|
||||
/* Hide the 2nd line in cases where it's empty
|
||||
(e.g. payer field with one or two fields requested) */
|
||||
display: none;
|
||||
}
|
||||
|
||||
address-option > .line > span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
address-option > .line > span:empty::before {
|
||||
/* Show the string for missing fields in grey when the field is empty */
|
||||
color: GrayText;
|
||||
content: attr(data-missing-string);
|
||||
}
|
@ -1,159 +0,0 @@
|
||||
/* 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-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
|
||||
import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
import RichOption from "./rich-option.js";
|
||||
/* import-globals-from ../unprivileged-fallbacks.js */
|
||||
|
||||
/**
|
||||
* Up to two-line address display. After bug 1475684 this will also be used for
|
||||
* the single-line <option> substitute too.
|
||||
*
|
||||
* <rich-select>
|
||||
* <address-option guid="98hgvnbmytfc"
|
||||
* address-level1="MI"
|
||||
* address-level2="Some City"
|
||||
* email="foo@example.com"
|
||||
* country="USA"
|
||||
* name="Jared Wein"
|
||||
* postal-code="90210"
|
||||
* street-address="1234 Anywhere St"
|
||||
* tel="+1 650 555-5555"></address-option>
|
||||
* </rich-select>
|
||||
*
|
||||
* Attribute names follow FormAutofillStorage.jsm.
|
||||
*/
|
||||
|
||||
export default class AddressOption extends ObservedPropertiesMixin(RichOption) {
|
||||
static get recordAttributes() {
|
||||
return [
|
||||
"address-level1",
|
||||
"address-level2",
|
||||
"address-level3",
|
||||
"country",
|
||||
"email",
|
||||
"guid",
|
||||
"name",
|
||||
"organization",
|
||||
"postal-code",
|
||||
"street-address",
|
||||
"tel",
|
||||
];
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return RichOption.observedAttributes.concat(
|
||||
AddressOption.recordAttributes,
|
||||
"address-fields",
|
||||
"break-after-nth-field",
|
||||
"data-field-separator"
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._line1 = document.createElement("div");
|
||||
this._line1.classList.add("line");
|
||||
this._line2 = document.createElement("div");
|
||||
this._line2.classList.add("line");
|
||||
|
||||
for (let name of AddressOption.recordAttributes) {
|
||||
this[`_${name}`] = document.createElement("span");
|
||||
this[`_${name}`].classList.add(name);
|
||||
// XXX Bug 1490816: Use appropriate strings
|
||||
let missingValueString =
|
||||
name.replace(/(-|^)([a-z])/g, ($0, $1, $2) => {
|
||||
return $1.replace("-", " ") + $2.toUpperCase();
|
||||
}) + " Missing";
|
||||
this[`_${name}`].dataset.missingString = missingValueString;
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.appendChild(this._line1);
|
||||
this.appendChild(this._line2);
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
static formatSingleLineLabel(address, addressFields) {
|
||||
return PaymentDialogUtils.getAddressLabel(address, addressFields);
|
||||
}
|
||||
|
||||
get requiredFields() {
|
||||
if (this.hasAttribute("address-fields")) {
|
||||
let names = this.getAttribute("address-fields")
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
if (names.length) {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
// "address-level1", // TODO: bug 1481481 - not required for some countries e.g. DE
|
||||
"address-level2",
|
||||
"country",
|
||||
"name",
|
||||
"postal-code",
|
||||
"street-address",
|
||||
];
|
||||
}
|
||||
|
||||
render() {
|
||||
// Clear the lines of the fields so we can append only the ones still
|
||||
// visible in the correct order below.
|
||||
this._line1.textContent = "";
|
||||
this._line2.textContent = "";
|
||||
|
||||
// Fill the fields with their text/strings.
|
||||
// Fall back to empty strings to prevent 'null' from appearing.
|
||||
for (let name of AddressOption.recordAttributes) {
|
||||
let camelCaseName = super.constructor.kebabToCamelCase(name);
|
||||
let fieldEl = this[`_${name}`];
|
||||
fieldEl.textContent = this[camelCaseName] || "";
|
||||
}
|
||||
|
||||
let { fieldsOrder } = PaymentDialogUtils.getFormFormat(this.country);
|
||||
// A subset of the requested fields may be returned if the fields don't apply to the country.
|
||||
let requestedVisibleFields = this.addressFields || "mailing-address";
|
||||
let visibleFields = EditAddress.computeVisibleFields(
|
||||
fieldsOrder,
|
||||
requestedVisibleFields
|
||||
);
|
||||
let visibleFieldCount = 0;
|
||||
let requiredFields = this.requiredFields;
|
||||
// Start by populating line 1
|
||||
let lineEl = this._line1;
|
||||
// Which field number to start line 2 after.
|
||||
let breakAfterNthField = this.breakAfterNthField || 2;
|
||||
|
||||
// Now actually place the fields in the proper place on the lines.
|
||||
for (let field of visibleFields) {
|
||||
let fieldEl = this[`_${field.fieldId}`];
|
||||
if (!fieldEl) {
|
||||
log.warn(`address-option render: '${field.fieldId}' doesn't exist`);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!fieldEl.textContent && !requiredFields.includes(field.fieldId)) {
|
||||
// The field is empty and we don't need to show "Missing …" so don't append.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (lineEl.children.length > 0) {
|
||||
lineEl.append(this.dataset.fieldSeparator);
|
||||
}
|
||||
lineEl.appendChild(fieldEl);
|
||||
|
||||
// Add a break after this field, if requested.
|
||||
if (++visibleFieldCount == breakAfterNthField) {
|
||||
lineEl = this._line2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("address-option", AddressOption);
|
@ -1,40 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
basic-card-option {
|
||||
grid-column-gap: 1em;
|
||||
grid-template-areas: "cc-type cc-number cc-exp cc-name";
|
||||
/* Need to set a minimum width for the cc-type svg in the <img> to fill */
|
||||
grid-template-columns: minmax(1em, auto);
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
basic-card-option > .cc-number,
|
||||
basic-card-option > .cc-name,
|
||||
basic-card-option > .cc-exp,
|
||||
basic-card-option > .cc-type {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
basic-card-option > .cc-number {
|
||||
grid-area: cc-number;
|
||||
/* Don't truncate the card number */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
basic-card-option > .cc-name {
|
||||
grid-area: cc-name;
|
||||
}
|
||||
|
||||
basic-card-option > .cc-exp {
|
||||
grid-area: cc-exp;
|
||||
}
|
||||
|
||||
basic-card-option > .cc-type {
|
||||
grid-area: cc-type;
|
||||
height: 100%;
|
||||
text-transform: capitalize;
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
/* 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 ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
import RichOption from "./rich-option.js";
|
||||
|
||||
/**
|
||||
* <rich-select>
|
||||
* <basic-card-option></basic-card-option>
|
||||
* </rich-select>
|
||||
*/
|
||||
|
||||
export default class BasicCardOption extends ObservedPropertiesMixin(
|
||||
RichOption
|
||||
) {
|
||||
static get recordAttributes() {
|
||||
return ["cc-exp", "cc-name", "cc-number", "cc-type", "guid"];
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return RichOption.observedAttributes.concat(
|
||||
BasicCardOption.recordAttributes
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
for (let name of ["cc-name", "cc-number", "cc-exp", "cc-type"]) {
|
||||
this[`_${name}`] = document.createElement(
|
||||
name == "cc-type" ? "img" : "span"
|
||||
);
|
||||
this[`_${name}`].classList.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
for (let name of ["cc-name", "cc-number", "cc-exp", "cc-type"]) {
|
||||
this.appendChild(this[`_${name}`]);
|
||||
}
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
static formatCCNumber(ccNumber) {
|
||||
// XXX: Bug 1470175 - This should probably be unified with CreditCard.jsm logic.
|
||||
return ccNumber ? ccNumber.replace(/[*]{4,}/, "****") : "";
|
||||
}
|
||||
|
||||
static formatSingleLineLabel(basicCard) {
|
||||
let ccNumber = BasicCardOption.formatCCNumber(basicCard["cc-number"]);
|
||||
|
||||
// XXX Bug 1473772 - Hard-coded string
|
||||
let ccExp = basicCard["cc-exp"] ? "Exp. " + basicCard["cc-exp"] : "";
|
||||
let ccName = basicCard["cc-name"];
|
||||
// XXX: Bug 1491040, displaying cc-type in this context may need its own localized string
|
||||
let ccType = basicCard["cc-type"] || "";
|
||||
// Filter out empty/undefined tokens before joining by three spaces
|
||||
// ( in the middle of two normal spaces to avoid them visually collapsing in HTML)
|
||||
return [
|
||||
ccType.replace(/^[a-z]/, $0 => $0.toUpperCase()),
|
||||
ccNumber,
|
||||
ccExp,
|
||||
ccName,
|
||||
// XXX Bug 1473772 - Hard-coded string:
|
||||
]
|
||||
.filter(str => !!str)
|
||||
.join(" \xa0 ");
|
||||
}
|
||||
|
||||
get requiredFields() {
|
||||
return BasicCardOption.recordAttributes;
|
||||
}
|
||||
|
||||
render() {
|
||||
this["_cc-name"].textContent = this.ccName || "";
|
||||
this["_cc-number"].textContent = BasicCardOption.formatCCNumber(
|
||||
this.ccNumber
|
||||
);
|
||||
// XXX Bug 1473772 - Hard-coded string:
|
||||
this["_cc-exp"].textContent = this.ccExp ? "Exp. " + this.ccExp : "";
|
||||
// XXX: Bug 1491040, displaying cc-type in this context may need its own localized string
|
||||
this["_cc-type"].alt = this.ccType || "";
|
||||
this["_cc-type"].src =
|
||||
"chrome://formautofill/content/icon-credit-card-generic.svg";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("basic-card-option", BasicCardOption);
|
@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 32">
|
||||
<rect x="0" y="0" width="48" height="32" rx="4" ry="4" fill="#000" fill-opacity="context-fill-opacity">
|
||||
</rect>
|
||||
<rect x="0" y="6" width="48" height="20" fill="#fff" fill-opacity="1">
|
||||
</rect>
|
||||
</svg>
|
Before Width: | Height: | Size: 267 B |
@ -1,112 +0,0 @@
|
||||
/* 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 ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
|
||||
/**
|
||||
* <csc-input placeholder="CVV*"
|
||||
default-value="123"
|
||||
front-tooltip="Look on front of card for CSC"
|
||||
back-tooltip="Look on back of card for CSC"></csc-input>
|
||||
*/
|
||||
|
||||
export default class CscInput extends ObservedPropertiesMixin(HTMLElement) {
|
||||
static get observedAttributes() {
|
||||
return [
|
||||
"back-tooltip",
|
||||
"card-type",
|
||||
"default-value",
|
||||
"disabled",
|
||||
"front-tooltip",
|
||||
"placeholder",
|
||||
"value",
|
||||
];
|
||||
}
|
||||
constructor({ useAlwaysVisiblePlaceholder, inputId } = {}) {
|
||||
super();
|
||||
|
||||
this.useAlwaysVisiblePlaceholder = useAlwaysVisiblePlaceholder;
|
||||
|
||||
this._input = document.createElement("input");
|
||||
this._input.id = inputId || "";
|
||||
this._input.setAttribute("type", "text");
|
||||
this._input.autocomplete = "off";
|
||||
this._input.size = 3;
|
||||
this._input.required = true;
|
||||
// 3 or more digits
|
||||
this._input.pattern = "[0-9]{3,}";
|
||||
this._input.classList.add("security-code");
|
||||
if (useAlwaysVisiblePlaceholder) {
|
||||
this._label = document.createElement("span");
|
||||
this._label.dataset.localization = "cardCVV";
|
||||
this._label.className = "label-text";
|
||||
}
|
||||
this._tooltip = document.createElement("span");
|
||||
this._tooltip.className = "info-tooltip csc";
|
||||
this._tooltip.setAttribute("tabindex", "0");
|
||||
this._tooltip.setAttribute("role", "tooltip");
|
||||
|
||||
// The parent connectedCallback calls its render method before
|
||||
// our connectedCallback can run. This causes issues for parent
|
||||
// code that is looking for all the form elements. Thus, we
|
||||
// append the children during the constructor to make sure they
|
||||
// be part of the DOM sooner.
|
||||
this.appendChild(this._input);
|
||||
if (this.useAlwaysVisiblePlaceholder) {
|
||||
this.appendChild(this._label);
|
||||
}
|
||||
this.appendChild(this._tooltip);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.defaultValue) {
|
||||
let oldDefaultValue = this._input.defaultValue;
|
||||
this._input.defaultValue = this.defaultValue;
|
||||
if (this._input.defaultValue != oldDefaultValue) {
|
||||
// Setting defaultValue will place a value in the field
|
||||
// but doesn't trigger a 'change' event, which is needed
|
||||
// to update the Pay button state on the summary page.
|
||||
this._input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
} else {
|
||||
this._input.defaultValue = "";
|
||||
}
|
||||
if (this.value) {
|
||||
// Setting the value will trigger form validation
|
||||
// so only set the value if one has been provided.
|
||||
this._input.value = this.value;
|
||||
}
|
||||
if (this.useAlwaysVisiblePlaceholder) {
|
||||
this._label.textContent = this.placeholder || "";
|
||||
} else {
|
||||
this._input.placeholder = this.placeholder || "";
|
||||
}
|
||||
if (this.cardType == "amex") {
|
||||
this._tooltip.setAttribute("aria-label", this.frontTooltip || "");
|
||||
} else {
|
||||
this._tooltip.setAttribute("aria-label", this.backTooltip || "");
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._input.value;
|
||||
}
|
||||
|
||||
get isValid() {
|
||||
return this._input.validity.valid;
|
||||
}
|
||||
|
||||
set disabled(value) {
|
||||
// This is kept out of render() since callers
|
||||
// are expecting it to apply immediately.
|
||||
this._input.disabled = value;
|
||||
return !!value;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("csc-input", CscInput);
|
@ -1,63 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* <currency-amount value="7.5" currency="USD" display-code></currency-amount>
|
||||
*/
|
||||
|
||||
import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
|
||||
export default class CurrencyAmount extends ObservedPropertiesMixin(
|
||||
HTMLElement
|
||||
) {
|
||||
static get observedAttributes() {
|
||||
return ["currency", "display-code", "value"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._currencyAmountTextNode = document.createTextNode("");
|
||||
this._currencyCodeElement = document.createElement("span");
|
||||
this._currencyCodeElement.classList.add("currency-code");
|
||||
}
|
||||
|
||||
render() {
|
||||
this.append(this._currencyAmountTextNode, this._currencyCodeElement);
|
||||
let currencyAmount = "";
|
||||
let currencyCode = "";
|
||||
try {
|
||||
if (this.value && this.currency) {
|
||||
let number = Number.parseFloat(this.value);
|
||||
if (Number.isNaN(number) || !Number.isFinite(number)) {
|
||||
throw new RangeError("currency-amount value must be a finite number");
|
||||
}
|
||||
const symbolFormatter = new Intl.NumberFormat(navigator.languages, {
|
||||
style: "currency",
|
||||
currency: this.currency,
|
||||
currencyDisplay: "symbol",
|
||||
});
|
||||
currencyAmount = symbolFormatter.format(this.value);
|
||||
|
||||
if (this.displayCode !== null) {
|
||||
// XXX: Bug 1473772 will move the separator to a Fluent string.
|
||||
currencyAmount += " ";
|
||||
|
||||
const codeFormatter = new Intl.NumberFormat(navigator.languages, {
|
||||
style: "currency",
|
||||
currency: this.currency,
|
||||
currencyDisplay: "code",
|
||||
});
|
||||
let parts = codeFormatter.formatToParts(this.value);
|
||||
let currencyPart = parts.find(part => part.type == "currency");
|
||||
currencyCode = currencyPart.value;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this._currencyAmountTextNode.textContent = currencyAmount;
|
||||
this._currencyCodeElement.textContent = currencyCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("currency-amount", CurrencyAmount);
|
@ -1,59 +0,0 @@
|
||||
/* 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 ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
|
||||
/**
|
||||
* <labelled-checkbox label="Some label" value="The value"></labelled-checkbox>
|
||||
*/
|
||||
|
||||
export default class LabelledCheckbox extends ObservedPropertiesMixin(
|
||||
HTMLElement
|
||||
) {
|
||||
static get observedAttributes() {
|
||||
return ["infoTooltip", "form", "label", "value"];
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._label = document.createElement("label");
|
||||
this._labelSpan = document.createElement("span");
|
||||
this._infoTooltip = document.createElement("span");
|
||||
this._infoTooltip.className = "info-tooltip";
|
||||
this._infoTooltip.setAttribute("tabindex", "0");
|
||||
this._infoTooltip.setAttribute("role", "tooltip");
|
||||
this._checkbox = document.createElement("input");
|
||||
this._checkbox.type = "checkbox";
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.appendChild(this._label);
|
||||
this._label.appendChild(this._checkbox);
|
||||
this._label.appendChild(this._labelSpan);
|
||||
this._label.appendChild(this._infoTooltip);
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this._labelSpan.textContent = this.label;
|
||||
this._infoTooltip.setAttribute("aria-label", this.infoTooltip);
|
||||
// We don't use the ObservedPropertiesMixin behaviour because we want to be able to mirror
|
||||
// form="" but ObservedPropertiesMixin removes attributes when "".
|
||||
if (this.hasAttribute("form")) {
|
||||
this._checkbox.setAttribute("form", this.getAttribute("form"));
|
||||
} else {
|
||||
this._checkbox.removeAttribute("form");
|
||||
}
|
||||
}
|
||||
|
||||
get checked() {
|
||||
return this._checkbox.checked;
|
||||
}
|
||||
|
||||
set checked(value) {
|
||||
return (this._checkbox.checked = value);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("labelled-checkbox", LabelledCheckbox);
|
@ -1,8 +0,0 @@
|
||||
payment-details-item {
|
||||
margin: 1px 0;
|
||||
min-height: 2em;
|
||||
}
|
||||
|
||||
payment-details-item > currency-amount {
|
||||
text-align: end;
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* <ul>
|
||||
* <payment-details-item
|
||||
label="Some item"
|
||||
amount-value="1.00"
|
||||
amount-currency="USD"></payment-details-item>
|
||||
* </ul>
|
||||
*/
|
||||
|
||||
import CurrencyAmount from "./currency-amount.js";
|
||||
import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
|
||||
export default class PaymentDetailsItem extends ObservedPropertiesMixin(
|
||||
HTMLElement
|
||||
) {
|
||||
static get observedAttributes() {
|
||||
return ["label", "amount-currency", "amount-value"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._label = document.createElement("span");
|
||||
this._label.classList.add("label");
|
||||
this._currencyAmount = new CurrencyAmount();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.appendChild(this._label);
|
||||
this.appendChild(this._currencyAmount);
|
||||
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this._currencyAmount.value = this.amountValue;
|
||||
this._currencyAmount.currency = this.amountCurrency;
|
||||
this._label.textContent = this.label;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("payment-details-item", PaymentDetailsItem);
|
@ -1,36 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* <payment-request-page></payment-request-page>
|
||||
*/
|
||||
|
||||
export default class PaymentRequestPage extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.classList.add("page");
|
||||
|
||||
this.pageTitleHeading = document.createElement("h2");
|
||||
|
||||
// The body and footer may be pre-defined in the template so re-use them if they exist.
|
||||
this.body =
|
||||
this.querySelector(":scope > .page-body") ||
|
||||
document.createElement("div");
|
||||
this.body.classList.add("page-body");
|
||||
|
||||
this.footer =
|
||||
this.querySelector(":scope > footer") || document.createElement("footer");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// The heading goes inside the body so it scrolls.
|
||||
this.body.prepend(this.pageTitleHeading);
|
||||
this.appendChild(this.body);
|
||||
|
||||
this.appendChild(this.footer);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("payment-request-page", PaymentRequestPage);
|
@ -1,26 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* <rich-select>
|
||||
* <rich-option></rich-option>
|
||||
* </rich-select>
|
||||
*/
|
||||
|
||||
import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
|
||||
export default class RichOption extends ObservedPropertiesMixin(HTMLElement) {
|
||||
static get observedAttributes() {
|
||||
return ["selected", "value"];
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.classList.add("rich-option");
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {}
|
||||
}
|
||||
|
||||
customElements.define("rich-option", RichOption);
|
@ -1,58 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
rich-select {
|
||||
/* Include the padding in the max-width calculation so that we truncate rather
|
||||
than grow wider than 100% of the parent. */
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
/* Has to be the same as `payment-method-picker > input`: */
|
||||
margin: 10px 0;
|
||||
/* Padding for the dropmarker (copied from common.css) */
|
||||
padding-inline-end: 24px;
|
||||
position: relative;
|
||||
/* Don't allow the <rich-select> to grow wider than the container so that we
|
||||
truncate with text-overflow for long options instead. */
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Focusing on the underlying select element outlines the outer
|
||||
rich-select wrapper making it appear like rich-select is focused. */
|
||||
rich-select:focus-within > select {
|
||||
outline: 1px dotted var(--in-content-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* The HTML select element is hidden and placed on the rich-option
|
||||
* element to make it look like clicking on the rich-option element
|
||||
* in the closed state opens the HTML select dropdown. */
|
||||
rich-select > select {
|
||||
/* Hide the text from the closed state so that the text/layout from
|
||||
<rich-option> won't overlap it. The !important matches common.css. */
|
||||
color: transparent !important;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
rich-select > select > option {
|
||||
/* Reset the text color in the popup/open state */
|
||||
color: var(--in-content-text-color);
|
||||
}
|
||||
|
||||
.rich-option {
|
||||
display: grid;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.rich-select-selected-option {
|
||||
/* Clicks on the selected rich option should go to the <select> below to open the popup */
|
||||
pointer-events: none;
|
||||
/* Use position:relative so this is positioned on top of the <select> which
|
||||
also has position:relative. */
|
||||
position: relative;
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
/* 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 HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
import RichOption from "./rich-option.js";
|
||||
|
||||
/**
|
||||
* <rich-select>
|
||||
* <rich-option></rich-option>
|
||||
* </rich-select>
|
||||
*
|
||||
* Note: The only supported way to change the selected option is via the
|
||||
* `value` setter.
|
||||
*/
|
||||
export default class RichSelect extends HandleEventMixin(
|
||||
ObservedPropertiesMixin(HTMLElement)
|
||||
) {
|
||||
static get observedAttributes() {
|
||||
return ["disabled", "hidden"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.popupBox = document.createElement("select");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
// the popupBox element may change in between constructor and being connected
|
||||
// so wait until connected before listening to events on it
|
||||
this.popupBox.addEventListener("change", this);
|
||||
this.appendChild(this.popupBox);
|
||||
this.render();
|
||||
}
|
||||
|
||||
get selectedOption() {
|
||||
return this.getOptionByValue(this.value);
|
||||
}
|
||||
|
||||
get selectedRichOption() {
|
||||
// XXX: Bug 1475684 - This can be removed once `selectedOption` returns a
|
||||
// RichOption which extends HTMLOptionElement.
|
||||
return this.querySelector(":scope > .rich-select-selected-option");
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.popupBox.value;
|
||||
}
|
||||
|
||||
set value(guid) {
|
||||
this.popupBox.value = guid;
|
||||
this.render();
|
||||
}
|
||||
|
||||
getOptionByValue(value) {
|
||||
return this.popupBox.querySelector(
|
||||
`:scope > [value="${CSS.escape(value)}"]`
|
||||
);
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
// Since the render function depends on the popupBox's value, we need to
|
||||
// re-render if the value changes.
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
let selectedRichOption = this.querySelector(
|
||||
":scope > .rich-select-selected-option"
|
||||
);
|
||||
if (selectedRichOption) {
|
||||
selectedRichOption.remove();
|
||||
}
|
||||
|
||||
if (this.value) {
|
||||
let optionType = this.getAttribute("option-type");
|
||||
if (!selectedRichOption || selectedRichOption.localName != optionType) {
|
||||
selectedRichOption = document.createElement(optionType);
|
||||
}
|
||||
|
||||
let option = this.getOptionByValue(this.value);
|
||||
let attributeNames = selectedRichOption.constructor.observedAttributes;
|
||||
for (let attributeName of attributeNames) {
|
||||
let attributeValue = option.getAttribute(attributeName);
|
||||
if (attributeValue) {
|
||||
selectedRichOption.setAttribute(attributeName, attributeValue);
|
||||
} else {
|
||||
selectedRichOption.removeAttribute(attributeName);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedRichOption = new RichOption();
|
||||
selectedRichOption.textContent = "(None selected)"; // XXX: bug 1473772
|
||||
}
|
||||
selectedRichOption.classList.add("rich-select-selected-option");
|
||||
// Hide the rich-option from a11y tools since the native <select> will
|
||||
// already provide the selected option label.
|
||||
selectedRichOption.setAttribute("aria-hidden", "true");
|
||||
selectedRichOption = this.appendChild(selectedRichOption);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("rich-select", RichSelect);
|
@ -1,16 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
shipping-option.rich-option {
|
||||
display: block;
|
||||
/* Below properties are to support truncating with an ellipsis for long options */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
shipping-option > .label,
|
||||
shipping-option > .amount {
|
||||
white-space: nowrap;
|
||||
}
|
@ -1,65 +0,0 @@
|
||||
/* 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 CurrencyAmount from "./currency-amount.js";
|
||||
import ObservedPropertiesMixin from "../mixins/ObservedPropertiesMixin.js";
|
||||
import RichOption from "./rich-option.js";
|
||||
|
||||
/**
|
||||
* <rich-select>
|
||||
* <shipping-option></shipping-option>
|
||||
* </rich-select>
|
||||
*/
|
||||
|
||||
export default class ShippingOption extends ObservedPropertiesMixin(
|
||||
RichOption
|
||||
) {
|
||||
static get recordAttributes() {
|
||||
return ["label", "amount-currency", "amount-value"];
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return RichOption.observedAttributes.concat(
|
||||
ShippingOption.recordAttributes
|
||||
);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.amount = null;
|
||||
this._currencyAmount = new CurrencyAmount();
|
||||
this._currencyAmount.classList.add("amount");
|
||||
this._label = document.createElement("span");
|
||||
this._label.classList.add("label");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.appendChild(this._currencyAmount);
|
||||
this.append(" ");
|
||||
this.appendChild(this._label);
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
static formatSingleLineLabel(option) {
|
||||
let amount = new CurrencyAmount();
|
||||
amount.value = option.amount.value;
|
||||
amount.currency = option.amount.currency;
|
||||
amount.render();
|
||||
|
||||
return amount.textContent + " " + option.label;
|
||||
}
|
||||
|
||||
render() {
|
||||
this._label.textContent = this.label;
|
||||
this._currencyAmount.currency = this.amountCurrency;
|
||||
this._currencyAmount.value = this.amountValue;
|
||||
// Need to call render after setting these properties
|
||||
// if we want the amount to get displayed in the same
|
||||
// render pass as the label.
|
||||
this._currencyAmount.render();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("shipping-option", ShippingOption);
|
@ -1,55 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
.error-text {
|
||||
color: #fff;
|
||||
background-color: #d70022;
|
||||
border-radius: 2px;
|
||||
margin: 5px 3px 0 3px;
|
||||
/* The padding-top and padding-bottom are referenced by address-form.js */ /* TODO */
|
||||
padding: 5px 12px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
top: 100%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
/* ::before is the error on the error text panel */
|
||||
:-moz-any(input, textarea, select) ~ .error-text::before {
|
||||
background-color: #d70022;
|
||||
top: -7px;
|
||||
content: '.';
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
text-indent: -999px;
|
||||
transform: rotate(45deg);
|
||||
white-space: nowrap;
|
||||
width: 16px;
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
/* Position the arrow */
|
||||
.error-text:dir(ltr)::before {
|
||||
left: 12px
|
||||
}
|
||||
|
||||
.error-text:dir(rtl)::before {
|
||||
right: 12px
|
||||
}
|
||||
|
||||
:-moz-any(input, textarea, select):-moz-ui-invalid:focus ~ .error-text {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
address-form > footer > .cancel-button {
|
||||
/* When cancel is shown (during onboarding), it should always be on the left with a space after it */
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
address-form > footer > .back-button {
|
||||
/* When back is shown (outside onboarding) we want "Back <space> Add/Save" */
|
||||
/* Bug 1468153 may change the button ordering to match platform conventions */
|
||||
margin-right: auto;
|
||||
}
|
@ -1,447 +0,0 @@
|
||||
/* 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-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
|
||||
import LabelledCheckbox from "../components/labelled-checkbox.js";
|
||||
import PaymentRequestPage from "../components/payment-request-page.js";
|
||||
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
|
||||
import paymentRequest from "../paymentRequest.js";
|
||||
import HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
/* import-globals-from ../unprivileged-fallbacks.js */
|
||||
|
||||
/**
|
||||
* <address-form></address-form>
|
||||
*
|
||||
* Don't use document.getElementById or document.querySelector* to access form
|
||||
* elements, use querySelector on `this` or `this.form` instead so that elements
|
||||
* can be found before the element is connected.
|
||||
*
|
||||
* XXX: Bug 1446164 - This form isn't localized when used via this custom element
|
||||
* as it will be much easier to share the logic once we switch to Fluent.
|
||||
*/
|
||||
|
||||
export default class AddressForm extends HandleEventMixin(
|
||||
PaymentStateSubscriberMixin(PaymentRequestPage)
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.genericErrorText = document.createElement("div");
|
||||
this.genericErrorText.setAttribute("aria-live", "polite");
|
||||
this.genericErrorText.classList.add("page-error");
|
||||
|
||||
this.cancelButton = document.createElement("button");
|
||||
this.cancelButton.className = "cancel-button";
|
||||
this.cancelButton.addEventListener("click", this);
|
||||
|
||||
this.backButton = document.createElement("button");
|
||||
this.backButton.className = "back-button";
|
||||
this.backButton.addEventListener("click", this);
|
||||
|
||||
this.saveButton = document.createElement("button");
|
||||
this.saveButton.className = "save-button primary";
|
||||
this.saveButton.addEventListener("click", this);
|
||||
|
||||
this.persistCheckbox = new LabelledCheckbox();
|
||||
this.persistCheckbox.className = "persist-checkbox";
|
||||
|
||||
// Combination of AddressErrors and PayerErrors as keys
|
||||
this._errorFieldMap = {
|
||||
addressLine: "#street-address",
|
||||
city: "#address-level2",
|
||||
country: "#country",
|
||||
dependentLocality: "#address-level3",
|
||||
email: "#email",
|
||||
// Bug 1472283 is on file to support
|
||||
// additional-name and family-name.
|
||||
// XXX: For now payer name errors go on the family-name and address-errors
|
||||
// go on the given-name so they don't overwrite each other.
|
||||
name: "#family-name",
|
||||
organization: "#organization",
|
||||
phone: "#tel",
|
||||
postalCode: "#postal-code",
|
||||
// Bug 1472283 is on file to support
|
||||
// additional-name and family-name.
|
||||
recipient: "#given-name",
|
||||
region: "#address-level1",
|
||||
// Bug 1474905 is on file to properly support regionCode. See
|
||||
// full note in paymentDialogWrapper.js
|
||||
regionCode: "#address-level1",
|
||||
};
|
||||
|
||||
// The markup is shared with form autofill preferences.
|
||||
let url = "formautofill/editAddress.xhtml";
|
||||
this.promiseReady = this._fetchMarkup(url).then(doc => {
|
||||
this.form = doc.getElementById("form");
|
||||
return this.form;
|
||||
});
|
||||
}
|
||||
|
||||
_fetchMarkup(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "document";
|
||||
xhr.addEventListener("error", reject);
|
||||
xhr.addEventListener("load", evt => {
|
||||
resolve(xhr.response);
|
||||
});
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.promiseReady.then(form => {
|
||||
this.body.appendChild(form);
|
||||
|
||||
let record = undefined;
|
||||
this.formHandler = new EditAddress(
|
||||
{
|
||||
form,
|
||||
},
|
||||
record,
|
||||
{
|
||||
DEFAULT_REGION: PaymentDialogUtils.DEFAULT_REGION,
|
||||
getFormFormat: PaymentDialogUtils.getFormFormat,
|
||||
findAddressSelectOption: PaymentDialogUtils.findAddressSelectOption,
|
||||
countries: PaymentDialogUtils.countries,
|
||||
}
|
||||
);
|
||||
|
||||
// The EditAddress constructor adds `input` event listeners on the same element,
|
||||
// which update field validity. By adding our event listeners after this constructor,
|
||||
// validity will be updated before our handlers get the event
|
||||
this.form.addEventListener("input", this);
|
||||
this.form.addEventListener("invalid", this);
|
||||
this.form.addEventListener("change", this);
|
||||
|
||||
// The "invalid" event does not bubble and needs to be listened for on each
|
||||
// form element.
|
||||
for (let field of this.form.elements) {
|
||||
field.addEventListener("invalid", this);
|
||||
}
|
||||
|
||||
this.body.appendChild(this.persistCheckbox);
|
||||
this.body.appendChild(this.genericErrorText);
|
||||
|
||||
this.footer.appendChild(this.cancelButton);
|
||||
this.footer.appendChild(this.backButton);
|
||||
this.footer.appendChild(this.saveButton);
|
||||
// Only call the connected super callback(s) once our markup is fully
|
||||
// connected, including the shared form fetched asynchronously.
|
||||
super.connectedCallback();
|
||||
});
|
||||
}
|
||||
|
||||
render(state) {
|
||||
if (!this.id) {
|
||||
throw new Error("AddressForm without an id");
|
||||
}
|
||||
let record;
|
||||
let { page, [this.id]: addressPage } = state;
|
||||
|
||||
if (this.id && page && page.id !== this.id) {
|
||||
log.debug(`${this.id}: no need to further render inactive page`);
|
||||
return;
|
||||
}
|
||||
|
||||
let editing = !!addressPage.guid;
|
||||
this.cancelButton.textContent = this.dataset.cancelButtonLabel;
|
||||
this.backButton.textContent = this.dataset.backButtonLabel;
|
||||
if (editing) {
|
||||
this.saveButton.textContent = this.dataset.updateButtonLabel;
|
||||
} else {
|
||||
this.saveButton.textContent = this.dataset.nextButtonLabel;
|
||||
}
|
||||
|
||||
this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
|
||||
this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
|
||||
|
||||
this.backButton.hidden = page.onboardingWizard;
|
||||
this.cancelButton.hidden = !page.onboardingWizard;
|
||||
|
||||
this.pageTitleHeading.textContent = editing
|
||||
? this.dataset.titleEdit
|
||||
: this.dataset.titleAdd;
|
||||
this.genericErrorText.textContent = page.error;
|
||||
|
||||
let addresses = paymentRequest.getAddresses(state);
|
||||
|
||||
// If an address is selected we want to edit it.
|
||||
if (editing) {
|
||||
record = addresses[addressPage.guid];
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
"Trying to edit a non-existing address: " + addressPage.guid
|
||||
);
|
||||
}
|
||||
// When editing an existing record, prevent changes to persistence
|
||||
this.persistCheckbox.hidden = true;
|
||||
} else {
|
||||
let {
|
||||
saveAddressDefaultChecked,
|
||||
} = PaymentDialogUtils.getDefaultPreferences();
|
||||
if (typeof saveAddressDefaultChecked != "boolean") {
|
||||
throw new Error(`Unexpected non-boolean value for saveAddressDefaultChecked from
|
||||
PaymentDialogUtils.getDefaultPreferences(): ${typeof saveAddressDefaultChecked}`);
|
||||
}
|
||||
// Adding a new record: default persistence to the pref value when in a not-private session
|
||||
this.persistCheckbox.hidden = false;
|
||||
this.persistCheckbox.checked = state.isPrivate
|
||||
? false
|
||||
: saveAddressDefaultChecked;
|
||||
}
|
||||
|
||||
let selectedStateKey = this.getAttribute("selected-state-key").split("|");
|
||||
log.debug(`${this.id}#render got selectedStateKey: ${selectedStateKey}`);
|
||||
|
||||
if (addressPage.addressFields) {
|
||||
this.form.dataset.addressFields = addressPage.addressFields;
|
||||
} else {
|
||||
this.form.dataset.addressFields = "mailing-address tel";
|
||||
}
|
||||
this.formHandler.loadRecord(record);
|
||||
|
||||
// Add validation to some address fields
|
||||
this.updateRequiredState();
|
||||
|
||||
// Show merchant errors for the appropriate address form.
|
||||
let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
|
||||
state,
|
||||
selectedStateKey
|
||||
);
|
||||
for (let [errorName, errorSelector] of Object.entries(
|
||||
this._errorFieldMap
|
||||
)) {
|
||||
let errorText = "";
|
||||
// Never show errors on an 'add' screen as they would be for a different address.
|
||||
if (editing && merchantFieldErrors) {
|
||||
if (errorName == "region" || errorName == "regionCode") {
|
||||
errorText =
|
||||
merchantFieldErrors.regionCode || merchantFieldErrors.region || "";
|
||||
} else {
|
||||
errorText = merchantFieldErrors[errorName] || "";
|
||||
}
|
||||
}
|
||||
let container = this.form.querySelector(errorSelector + "-container");
|
||||
let field = this.form.querySelector(errorSelector);
|
||||
field.setCustomValidity(errorText);
|
||||
let span = paymentRequest.maybeCreateFieldErrorElement(container);
|
||||
span.textContent = errorText;
|
||||
}
|
||||
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
if (event.target.id == "country") {
|
||||
this.updateRequiredState();
|
||||
}
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
onInvalid(event) {
|
||||
if (event.target instanceof HTMLFormElement) {
|
||||
this.onInvalidForm(event);
|
||||
} else {
|
||||
this.onInvalidField(event);
|
||||
}
|
||||
}
|
||||
|
||||
onClick(evt) {
|
||||
switch (evt.target) {
|
||||
case this.cancelButton: {
|
||||
paymentRequest.cancel();
|
||||
break;
|
||||
}
|
||||
case this.backButton: {
|
||||
let currentState = this.requestStore.getState();
|
||||
const previousId = currentState.page.previousId;
|
||||
let state = {
|
||||
page: {
|
||||
id: previousId || "payment-summary",
|
||||
},
|
||||
};
|
||||
if (previousId) {
|
||||
state[previousId] = Object.assign({}, currentState[previousId], {
|
||||
preserveFieldValues: true,
|
||||
});
|
||||
}
|
||||
this.requestStore.setState(state);
|
||||
break;
|
||||
}
|
||||
case this.saveButton: {
|
||||
if (this.form.checkValidity()) {
|
||||
this.saveRecord();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unexpected click target");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
event.target.setCustomValidity("");
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event - "invalid" event
|
||||
* Note: Keep this in-sync with the equivalent version in basic-card-form.js
|
||||
*/
|
||||
onInvalidField(event) {
|
||||
let field = event.target;
|
||||
let container = field.closest(`#${field.id}-container`);
|
||||
let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
|
||||
errorTextSpan.textContent = field.validationMessage;
|
||||
}
|
||||
|
||||
onInvalidForm() {
|
||||
this.saveButton.disabled = true;
|
||||
}
|
||||
|
||||
updateRequiredState() {
|
||||
for (let field of this.form.elements) {
|
||||
let container = field.closest(`#${field.id}-container`);
|
||||
if (field.localName == "button" || !container) {
|
||||
continue;
|
||||
}
|
||||
let span = container.querySelector(".label-text");
|
||||
span.setAttribute(
|
||||
"fieldRequiredSymbol",
|
||||
this.dataset.fieldRequiredSymbol
|
||||
);
|
||||
container.toggleAttribute("required", field.required && !field.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
updateSaveButtonState() {
|
||||
this.saveButton.disabled = !this.form.checkValidity();
|
||||
}
|
||||
|
||||
async saveRecord() {
|
||||
let record = this.formHandler.buildFormObject();
|
||||
let currentState = this.requestStore.getState();
|
||||
let {
|
||||
page,
|
||||
tempAddresses,
|
||||
savedBasicCards,
|
||||
[this.id]: addressPage,
|
||||
} = currentState;
|
||||
let editing = !!addressPage.guid;
|
||||
|
||||
if (
|
||||
editing
|
||||
? addressPage.guid in tempAddresses
|
||||
: !this.persistCheckbox.checked
|
||||
) {
|
||||
record.isTemporary = true;
|
||||
}
|
||||
|
||||
let successStateChange;
|
||||
const previousId = page.previousId;
|
||||
if (page.onboardingWizard && !Object.keys(savedBasicCards).length) {
|
||||
successStateChange = {
|
||||
"basic-card-page": {
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
// Preserve field values as the user may have already edited the card
|
||||
// page and went back to the address page to make a correction.
|
||||
preserveFieldValues: true,
|
||||
},
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
previousId: this.id,
|
||||
onboardingWizard: page.onboardingWizard,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
successStateChange = {
|
||||
page: {
|
||||
id: previousId || "payment-summary",
|
||||
onboardingWizard: page.onboardingWizard,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (previousId) {
|
||||
successStateChange[previousId] = Object.assign(
|
||||
{},
|
||||
currentState[previousId]
|
||||
);
|
||||
successStateChange[previousId].preserveFieldValues = true;
|
||||
}
|
||||
|
||||
try {
|
||||
let { guid } = await paymentRequest.updateAutofillRecord(
|
||||
"addresses",
|
||||
record,
|
||||
addressPage.guid
|
||||
);
|
||||
let selectedStateKey = this.getAttribute("selected-state-key").split("|");
|
||||
|
||||
if (selectedStateKey.length == 1) {
|
||||
Object.assign(successStateChange, {
|
||||
[selectedStateKey[0]]: guid,
|
||||
});
|
||||
} else if (selectedStateKey.length == 2) {
|
||||
// Need to keep properties like preserveFieldValues from getting removed.
|
||||
let subObj = Object.assign({}, successStateChange[selectedStateKey[0]]);
|
||||
subObj[selectedStateKey[1]] = guid;
|
||||
Object.assign(successStateChange, {
|
||||
[selectedStateKey[0]]: subObj,
|
||||
});
|
||||
} else {
|
||||
throw new Error(
|
||||
`selectedStateKey not supported: '${selectedStateKey}'`
|
||||
);
|
||||
}
|
||||
|
||||
this.requestStore.setState(successStateChange);
|
||||
} catch (ex) {
|
||||
log.warn("saveRecord: error:", ex);
|
||||
this.requestStore.setState({
|
||||
page: {
|
||||
id: this.id,
|
||||
onboardingWizard: page.onboardingWizard,
|
||||
error: this.dataset.errorGenericSave,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dictionary of field-specific merchant errors relevant to the
|
||||
* specific form identified by the state key.
|
||||
* @param {object} state The application state
|
||||
* @param {string[]} stateKey The key in state to return address errors for.
|
||||
* @returns {object} with keys as PaymentRequest field names and values of
|
||||
* merchant-provided error strings.
|
||||
*/
|
||||
static merchantFieldErrorsForForm(state, stateKey) {
|
||||
let { paymentDetails } = state.request;
|
||||
switch (stateKey.join("|")) {
|
||||
case "selectedShippingAddress": {
|
||||
return paymentDetails.shippingAddressErrors;
|
||||
}
|
||||
case "selectedPayerAddress": {
|
||||
return paymentDetails.payerErrors;
|
||||
}
|
||||
case "basic-card-page|billingAddressGUID": {
|
||||
// `paymentMethod` can be null.
|
||||
return (
|
||||
(paymentDetails.paymentMethodErrors &&
|
||||
paymentDetails.paymentMethodErrors.billingAddress) ||
|
||||
{}
|
||||
);
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unknown selectedStateKey");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("address-form", AddressForm);
|
@ -1,282 +0,0 @@
|
||||
/* 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 AddressForm from "./address-form.js";
|
||||
import AddressOption from "../components/address-option.js";
|
||||
import RichPicker from "./rich-picker.js";
|
||||
import paymentRequest from "../paymentRequest.js";
|
||||
import HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
|
||||
/**
|
||||
* <address-picker></address-picker>
|
||||
* Container around add/edit links and <rich-select> with
|
||||
* <address-option> listening to savedAddresses & tempAddresses.
|
||||
*/
|
||||
|
||||
export default class AddressPicker extends HandleEventMixin(RichPicker) {
|
||||
static get pickerAttributes() {
|
||||
return ["address-fields", "break-after-nth-field", "data-field-separator"];
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return RichPicker.observedAttributes.concat(AddressPicker.pickerAttributes);
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.dropdown.setAttribute("option-type", "address-option");
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
super.attributeChangedCallback(name, oldValue, newValue);
|
||||
// connectedCallback may add and adjust elements & values
|
||||
// so avoid calling render before the element is connected
|
||||
if (
|
||||
this.isConnected &&
|
||||
AddressPicker.pickerAttributes.includes(name) &&
|
||||
oldValue !== newValue
|
||||
) {
|
||||
this.render(this.requestStore.getState());
|
||||
}
|
||||
}
|
||||
|
||||
get fieldNames() {
|
||||
if (this.hasAttribute("address-fields")) {
|
||||
let names = this.getAttribute("address-fields")
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
if (names.length) {
|
||||
return names;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
// "address-level1", // TODO: bug 1481481 - not required for some countries e.g. DE
|
||||
"address-level2",
|
||||
"country",
|
||||
"name",
|
||||
"postal-code",
|
||||
"street-address",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* De-dupe and filter addresses for the given set of fields that will be visible
|
||||
*
|
||||
* @param {object} addresses
|
||||
* @param {array?} fieldNames - optional list of field names that be used when
|
||||
* de-duping and excluding entries
|
||||
* @returns {object} filtered copy of given addresses
|
||||
*/
|
||||
filterAddresses(addresses, fieldNames = this.fieldNames) {
|
||||
let uniques = new Set();
|
||||
let result = {};
|
||||
for (let [guid, address] of Object.entries(addresses)) {
|
||||
let addressCopy = {};
|
||||
let isMatch = false;
|
||||
// exclude addresses that are missing all of the requested fields
|
||||
for (let name of fieldNames) {
|
||||
if (address[name]) {
|
||||
isMatch = true;
|
||||
addressCopy[name] = address[name];
|
||||
}
|
||||
}
|
||||
if (isMatch) {
|
||||
let key = JSON.stringify(addressCopy);
|
||||
// exclude duplicated addresses
|
||||
if (!uniques.has(key)) {
|
||||
uniques.add(key);
|
||||
result[guid] = address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get options() {
|
||||
return this.dropdown.popupBox.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} state - See `PaymentsStore.setState`
|
||||
* The value of the picker is retrieved from state store rather than the DOM
|
||||
* @returns {string} guid
|
||||
*/
|
||||
getCurrentValue(state) {
|
||||
let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
|
||||
let guid = state[selectedKey];
|
||||
if (selectedLeaf) {
|
||||
guid = guid[selectedLeaf];
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
|
||||
render(state) {
|
||||
let selectedAddressGUID = this.getCurrentValue(state) || "";
|
||||
let addresses = paymentRequest.getAddresses(state);
|
||||
let desiredOptions = [];
|
||||
let filteredAddresses = this.filterAddresses(addresses, this.fieldNames);
|
||||
for (let [guid, address] of Object.entries(filteredAddresses)) {
|
||||
let optionEl = this.dropdown.getOptionByValue(guid);
|
||||
if (!optionEl) {
|
||||
optionEl = document.createElement("option");
|
||||
optionEl.value = guid;
|
||||
}
|
||||
|
||||
for (let key of AddressOption.recordAttributes) {
|
||||
let val = address[key];
|
||||
if (val) {
|
||||
optionEl.setAttribute(key, val);
|
||||
} else {
|
||||
optionEl.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
|
||||
optionEl.dataset.fieldSeparator = this.dataset.fieldSeparator;
|
||||
|
||||
if (this.hasAttribute("address-fields")) {
|
||||
optionEl.setAttribute(
|
||||
"address-fields",
|
||||
this.getAttribute("address-fields")
|
||||
);
|
||||
} else {
|
||||
optionEl.removeAttribute("address-fields");
|
||||
}
|
||||
|
||||
if (this.hasAttribute("break-after-nth-field")) {
|
||||
optionEl.setAttribute(
|
||||
"break-after-nth-field",
|
||||
this.getAttribute("break-after-nth-field")
|
||||
);
|
||||
} else {
|
||||
optionEl.removeAttribute("break-after-nth-field");
|
||||
}
|
||||
|
||||
// fieldNames getter is not used here because it returns a default array with
|
||||
// attributes even when "address-fields" observed attribute is null.
|
||||
let addressFields = this.getAttribute("address-fields");
|
||||
optionEl.textContent = AddressOption.formatSingleLineLabel(
|
||||
address,
|
||||
addressFields
|
||||
);
|
||||
desiredOptions.push(optionEl);
|
||||
}
|
||||
|
||||
this.dropdown.popupBox.textContent = "";
|
||||
|
||||
if (this._allowEmptyOption) {
|
||||
let optionEl = document.createElement("option");
|
||||
optionEl.value = "";
|
||||
desiredOptions.unshift(optionEl);
|
||||
}
|
||||
|
||||
for (let option of desiredOptions) {
|
||||
this.dropdown.popupBox.appendChild(option);
|
||||
}
|
||||
|
||||
// Update selectedness after the options are updated
|
||||
this.dropdown.value = selectedAddressGUID;
|
||||
|
||||
if (selectedAddressGUID && selectedAddressGUID !== this.dropdown.value) {
|
||||
throw new Error(
|
||||
`${this.selectedStateKey} option ${selectedAddressGUID} ` +
|
||||
`does not exist in the address picker`
|
||||
);
|
||||
}
|
||||
|
||||
super.render(state);
|
||||
}
|
||||
|
||||
get selectedStateKey() {
|
||||
return this.getAttribute("selected-state-key");
|
||||
}
|
||||
|
||||
errorForSelectedOption(state) {
|
||||
let superError = super.errorForSelectedOption(state);
|
||||
if (superError) {
|
||||
return superError;
|
||||
}
|
||||
|
||||
if (!this.selectedOption) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let merchantFieldErrors = AddressForm.merchantFieldErrorsForForm(
|
||||
state,
|
||||
this.selectedStateKey.split("|")
|
||||
);
|
||||
// TODO: errors in priority order.
|
||||
return (
|
||||
Object.values(merchantFieldErrors).find(msg => {
|
||||
return typeof msg == "string" && msg.length;
|
||||
}) || ""
|
||||
);
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
let [selectedKey, selectedLeaf] = this.selectedStateKey.split("|");
|
||||
if (!selectedKey) {
|
||||
return;
|
||||
}
|
||||
// selectedStateKey can be a '|' delimited string indicating a path into the state object
|
||||
// to update with the new value
|
||||
let newState = {};
|
||||
|
||||
if (selectedLeaf) {
|
||||
let currentState = this.requestStore.getState();
|
||||
newState[selectedKey] = Object.assign({}, currentState[selectedKey], {
|
||||
[selectedLeaf]: this.dropdown.value,
|
||||
});
|
||||
} else {
|
||||
newState[selectedKey] = this.dropdown.value;
|
||||
}
|
||||
this.requestStore.setState(newState);
|
||||
}
|
||||
|
||||
onClick({ target }) {
|
||||
let pageId;
|
||||
let currentState = this.requestStore.getState();
|
||||
let nextState = {
|
||||
page: {},
|
||||
};
|
||||
|
||||
switch (this.selectedStateKey) {
|
||||
case "selectedShippingAddress":
|
||||
pageId = "shipping-address-page";
|
||||
break;
|
||||
case "selectedPayerAddress":
|
||||
pageId = "payer-address-page";
|
||||
break;
|
||||
case "basic-card-page|billingAddressGUID":
|
||||
pageId = "billing-address-page";
|
||||
break;
|
||||
default: {
|
||||
throw new Error(
|
||||
"onClick, un-matched selectedStateKey: " + this.selectedStateKey
|
||||
);
|
||||
}
|
||||
}
|
||||
nextState.page.id = pageId;
|
||||
let addressFields = this.getAttribute("address-fields");
|
||||
nextState[pageId] = { addressFields };
|
||||
|
||||
switch (target) {
|
||||
case this.addLink: {
|
||||
nextState[pageId].guid = null;
|
||||
break;
|
||||
}
|
||||
case this.editLink: {
|
||||
nextState[pageId].guid = this.getCurrentValue(currentState);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unexpected onClick");
|
||||
}
|
||||
}
|
||||
|
||||
this.requestStore.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("address-picker", AddressPicker);
|
@ -1,43 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
basic-card-form .editCreditCardForm {
|
||||
/* Add the persist-checkbox row to the grid */
|
||||
grid-template-areas:
|
||||
"cc-number cc-exp-month cc-exp-year"
|
||||
"cc-name cc-type cc-csc"
|
||||
"accepted accepted accepted"
|
||||
"persist-checkbox persist-checkbox persist-checkbox"
|
||||
"billingAddressGUID billingAddressGUID billingAddressGUID";
|
||||
}
|
||||
|
||||
basic-card-form csc-input {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
basic-card-form .editCreditCardForm > accepted-cards {
|
||||
grid-area: accepted;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
basic-card-form .editCreditCardForm .persist-checkbox {
|
||||
display: flex;
|
||||
grid-area: persist-checkbox;
|
||||
}
|
||||
|
||||
#billingAddressGUID-container {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
basic-card-form > footer > .cancel-button {
|
||||
/* When cancel is shown (during onboarding), it should always be on the left with a space after it */
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
basic-card-form > footer > .cancel-button[hidden] ~ .back-button {
|
||||
/* When back is shown (outside onboarding) we want "Back <space> Add/Save" */
|
||||
/* Bug 1468153 may change the button ordering to match platform conventions */
|
||||
margin-right: auto;
|
||||
}
|
@ -1,507 +0,0 @@
|
||||
/* 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-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/
|
||||
import AcceptedCards from "../components/accepted-cards.js";
|
||||
import BillingAddressPicker from "./billing-address-picker.js";
|
||||
import CscInput from "../components/csc-input.js";
|
||||
import LabelledCheckbox from "../components/labelled-checkbox.js";
|
||||
import PaymentRequestPage from "../components/payment-request-page.js";
|
||||
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
|
||||
import paymentRequest from "../paymentRequest.js";
|
||||
import HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
|
||||
/* import-globals-from ../unprivileged-fallbacks.js */
|
||||
|
||||
/**
|
||||
* <basic-card-form></basic-card-form>
|
||||
*
|
||||
* XXX: Bug 1446164 - This form isn't localized when used via this custom element
|
||||
* as it will be much easier to share the logic once we switch to Fluent.
|
||||
*/
|
||||
|
||||
export default class BasicCardForm extends HandleEventMixin(
|
||||
PaymentStateSubscriberMixin(PaymentRequestPage)
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.genericErrorText = document.createElement("div");
|
||||
this.genericErrorText.setAttribute("aria-live", "polite");
|
||||
this.genericErrorText.classList.add("page-error");
|
||||
|
||||
this.cscInput = new CscInput({
|
||||
useAlwaysVisiblePlaceholder: true,
|
||||
inputId: "cc-csc",
|
||||
});
|
||||
|
||||
this.persistCheckbox = new LabelledCheckbox();
|
||||
// The persist checkbox shouldn't be part of the record which gets saved so
|
||||
// exclude it from the form.
|
||||
this.persistCheckbox.form = "";
|
||||
this.persistCheckbox.className = "persist-checkbox";
|
||||
|
||||
this.acceptedCardsList = new AcceptedCards();
|
||||
|
||||
// page footer
|
||||
this.cancelButton = document.createElement("button");
|
||||
this.cancelButton.className = "cancel-button";
|
||||
this.cancelButton.addEventListener("click", this);
|
||||
|
||||
this.backButton = document.createElement("button");
|
||||
this.backButton.className = "back-button";
|
||||
this.backButton.addEventListener("click", this);
|
||||
|
||||
this.saveButton = document.createElement("button");
|
||||
this.saveButton.className = "save-button primary";
|
||||
this.saveButton.addEventListener("click", this);
|
||||
|
||||
this.footer.append(this.cancelButton, this.backButton, this.saveButton);
|
||||
|
||||
// The markup is shared with form autofill preferences.
|
||||
let url = "formautofill/editCreditCard.xhtml";
|
||||
this.promiseReady = this._fetchMarkup(url).then(doc => {
|
||||
this.form = doc.getElementById("form");
|
||||
return this.form;
|
||||
});
|
||||
}
|
||||
|
||||
_fetchMarkup(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.responseType = "document";
|
||||
xhr.addEventListener("error", reject);
|
||||
xhr.addEventListener("load", evt => {
|
||||
resolve(xhr.response);
|
||||
});
|
||||
xhr.open("GET", url);
|
||||
xhr.send();
|
||||
});
|
||||
}
|
||||
|
||||
_upgradeBillingAddressPicker() {
|
||||
let addressRow = this.form.querySelector(".billingAddressRow");
|
||||
let addressPicker = (this.billingAddressPicker = new BillingAddressPicker());
|
||||
|
||||
// Wrap the existing <select> that the formHandler manages
|
||||
if (addressPicker.dropdown.popupBox) {
|
||||
addressPicker.dropdown.popupBox.remove();
|
||||
}
|
||||
addressPicker.dropdown.popupBox = this.form.querySelector(
|
||||
"#billingAddressGUID"
|
||||
);
|
||||
|
||||
// Hide the original label as the address picker provide its own,
|
||||
// but we'll copy the localized textContent from it when rendering
|
||||
addressRow.querySelector(".label-text").hidden = true;
|
||||
|
||||
addressPicker.dataset.addLinkLabel = this.dataset.addressAddLinkLabel;
|
||||
addressPicker.dataset.editLinkLabel = this.dataset.addressEditLinkLabel;
|
||||
addressPicker.dataset.fieldSeparator = this.dataset.addressFieldSeparator;
|
||||
addressPicker.dataset.addAddressTitle = this.dataset.billingAddressTitleAdd;
|
||||
addressPicker.dataset.editAddressTitle = this.dataset.billingAddressTitleEdit;
|
||||
addressPicker.dataset.invalidLabel = this.dataset.invalidAddressLabel;
|
||||
// break-after-nth-field, address-fields not needed here
|
||||
|
||||
// this state is only used to carry the selected guid between pages;
|
||||
// the select#billingAddressGUID is the source of truth for the current value
|
||||
addressPicker.setAttribute(
|
||||
"selected-state-key",
|
||||
"basic-card-page|billingAddressGUID"
|
||||
);
|
||||
|
||||
addressPicker.addLink.addEventListener("click", this);
|
||||
addressPicker.editLink.addEventListener("click", this);
|
||||
|
||||
addressRow.appendChild(addressPicker);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.promiseReady.then(form => {
|
||||
this.body.appendChild(form);
|
||||
|
||||
let record = {};
|
||||
let addresses = [];
|
||||
this.formHandler = new EditCreditCard(
|
||||
{
|
||||
form,
|
||||
},
|
||||
record,
|
||||
addresses,
|
||||
{
|
||||
isCCNumber: PaymentDialogUtils.isCCNumber,
|
||||
getAddressLabel: PaymentDialogUtils.getAddressLabel,
|
||||
getSupportedNetworks: PaymentDialogUtils.getCreditCardNetworks,
|
||||
}
|
||||
);
|
||||
|
||||
// The EditCreditCard constructor adds `change` and `input` event listeners on the same
|
||||
// element, which update field validity. By adding our event listeners after this
|
||||
// constructor, validity will be updated before our handlers get the event
|
||||
form.addEventListener("change", this);
|
||||
form.addEventListener("input", this);
|
||||
form.addEventListener("invalid", this);
|
||||
|
||||
this._upgradeBillingAddressPicker();
|
||||
|
||||
// The "invalid" event does not bubble and needs to be listened for on each
|
||||
// form element.
|
||||
for (let field of this.form.elements) {
|
||||
field.addEventListener("invalid", this);
|
||||
}
|
||||
|
||||
// Replace the form-autofill cc-csc fields with our csc-input.
|
||||
let cscContainer = this.form.querySelector("#cc-csc-container");
|
||||
cscContainer.textContent = "";
|
||||
cscContainer.appendChild(this.cscInput);
|
||||
|
||||
let billingAddressRow = this.form.querySelector(".billingAddressRow");
|
||||
form.insertBefore(this.persistCheckbox, billingAddressRow);
|
||||
form.insertBefore(this.acceptedCardsList, billingAddressRow);
|
||||
this.body.appendChild(this.genericErrorText);
|
||||
// Only call the connected super callback(s) once our markup is fully
|
||||
// connected, including the shared form fetched asynchronously.
|
||||
super.connectedCallback();
|
||||
});
|
||||
}
|
||||
|
||||
render(state) {
|
||||
let {
|
||||
page,
|
||||
selectedShippingAddress,
|
||||
"basic-card-page": basicCardPage,
|
||||
} = state;
|
||||
|
||||
if (this.id && page && page.id !== this.id) {
|
||||
log.debug(
|
||||
`BasicCardForm: no need to further render inactive page: ${page.id}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!basicCardPage.selectedStateKey) {
|
||||
throw new Error("A `selectedStateKey` is required");
|
||||
}
|
||||
|
||||
let editing = !!basicCardPage.guid;
|
||||
this.cancelButton.textContent = this.dataset.cancelButtonLabel;
|
||||
this.backButton.textContent = this.dataset.backButtonLabel;
|
||||
if (editing) {
|
||||
this.saveButton.textContent = this.dataset.updateButtonLabel;
|
||||
} else {
|
||||
this.saveButton.textContent = this.dataset.nextButtonLabel;
|
||||
}
|
||||
|
||||
this.cscInput.placeholder = this.dataset.cscPlaceholder;
|
||||
this.cscInput.frontTooltip = this.dataset.cscFrontInfoTooltip;
|
||||
this.cscInput.backTooltip = this.dataset.cscBackInfoTooltip;
|
||||
|
||||
// The label text from the form isn't available until render() time.
|
||||
let labelText = this.form.querySelector(".billingAddressRow .label-text")
|
||||
.textContent;
|
||||
this.billingAddressPicker.setAttribute("label", labelText);
|
||||
|
||||
this.persistCheckbox.label = this.dataset.persistCheckboxLabel;
|
||||
this.persistCheckbox.infoTooltip = this.dataset.persistCheckboxInfoTooltip;
|
||||
|
||||
this.acceptedCardsList.label = this.dataset.acceptedCardsLabel;
|
||||
|
||||
// The next line needs an onboarding check since we don't set previousId
|
||||
// when navigating to add/edit directly from the summary page.
|
||||
this.backButton.hidden = !page.previousId && page.onboardingWizard;
|
||||
this.cancelButton.hidden = !page.onboardingWizard;
|
||||
|
||||
let record = {};
|
||||
let basicCards = paymentRequest.getBasicCards(state);
|
||||
let addresses = paymentRequest.getAddresses(state);
|
||||
|
||||
this.genericErrorText.textContent = page.error;
|
||||
|
||||
this.form.querySelector("#cc-number").disabled = editing;
|
||||
|
||||
// The CVV fields should be hidden and disabled when editing.
|
||||
this.form.querySelector("#cc-csc-container").hidden = editing;
|
||||
this.cscInput.disabled = editing;
|
||||
|
||||
// If a card is selected we want to edit it.
|
||||
if (editing) {
|
||||
this.pageTitleHeading.textContent = this.dataset.editBasicCardTitle;
|
||||
record = basicCards[basicCardPage.guid];
|
||||
if (!record) {
|
||||
throw new Error(
|
||||
"Trying to edit a non-existing card: " + basicCardPage.guid
|
||||
);
|
||||
}
|
||||
// When editing an existing record, prevent changes to persistence
|
||||
this.persistCheckbox.hidden = true;
|
||||
} else {
|
||||
this.pageTitleHeading.textContent = this.dataset.addBasicCardTitle;
|
||||
// Use a currently selected shipping address as the default billing address
|
||||
record.billingAddressGUID = basicCardPage.billingAddressGUID;
|
||||
if (!record.billingAddressGUID && selectedShippingAddress) {
|
||||
record.billingAddressGUID = selectedShippingAddress;
|
||||
}
|
||||
|
||||
let {
|
||||
saveCreditCardDefaultChecked,
|
||||
} = PaymentDialogUtils.getDefaultPreferences();
|
||||
if (typeof saveCreditCardDefaultChecked != "boolean") {
|
||||
throw new Error(`Unexpected non-boolean value for saveCreditCardDefaultChecked from
|
||||
PaymentDialogUtils.getDefaultPreferences(): ${typeof saveCreditCardDefaultChecked}`);
|
||||
}
|
||||
// Adding a new record: default persistence to pref value when in a not-private session
|
||||
this.persistCheckbox.hidden = false;
|
||||
if (basicCardPage.hasOwnProperty("persistCheckboxValue")) {
|
||||
// returning to this page, use previous checked state
|
||||
this.persistCheckbox.checked = basicCardPage.persistCheckboxValue;
|
||||
} else {
|
||||
this.persistCheckbox.checked = state.isPrivate
|
||||
? false
|
||||
: saveCreditCardDefaultChecked;
|
||||
}
|
||||
}
|
||||
|
||||
this.formHandler.loadRecord(
|
||||
record,
|
||||
addresses,
|
||||
basicCardPage.preserveFieldValues
|
||||
);
|
||||
|
||||
this.form.querySelector(".billingAddressRow").hidden = false;
|
||||
|
||||
let billingAddressSelect = this.billingAddressPicker.dropdown;
|
||||
if (basicCardPage.billingAddressGUID) {
|
||||
billingAddressSelect.value = basicCardPage.billingAddressGUID;
|
||||
} else if (!editing) {
|
||||
if (paymentRequest.getAddresses(state)[selectedShippingAddress]) {
|
||||
billingAddressSelect.value = selectedShippingAddress;
|
||||
} else {
|
||||
let firstAddressGUID = Object.keys(addresses)[0];
|
||||
if (firstAddressGUID) {
|
||||
// Only set the value if we have a saved address to not mark the field
|
||||
// dirty and invalid on an add form with no saved addresses.
|
||||
billingAddressSelect.value = firstAddressGUID;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Need to recalculate the populated state since
|
||||
// billingAddressSelect is updated after loadRecord.
|
||||
this.formHandler.updatePopulatedState(billingAddressSelect.popupBox);
|
||||
|
||||
this.updateRequiredState();
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
onChange(evt) {
|
||||
let ccType = this.form.querySelector("#cc-type");
|
||||
this.cscInput.setAttribute("card-type", ccType.value);
|
||||
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
onClick(evt) {
|
||||
switch (evt.target) {
|
||||
case this.cancelButton: {
|
||||
paymentRequest.cancel();
|
||||
break;
|
||||
}
|
||||
case this.billingAddressPicker.addLink:
|
||||
case this.billingAddressPicker.editLink: {
|
||||
// The address-picker has set state for the page to advance to, now set up the
|
||||
// necessary state for returning to and re-rendering this page
|
||||
let {
|
||||
"basic-card-page": basicCardPage,
|
||||
page,
|
||||
} = this.requestStore.getState();
|
||||
let nextState = {
|
||||
page: Object.assign({}, page, {
|
||||
previousId: "basic-card-page",
|
||||
}),
|
||||
"basic-card-page": {
|
||||
preserveFieldValues: true,
|
||||
guid: basicCardPage.guid,
|
||||
persistCheckboxValue: this.persistCheckbox.checked,
|
||||
selectedStateKey: basicCardPage.selectedStateKey,
|
||||
},
|
||||
};
|
||||
this.requestStore.setState(nextState);
|
||||
break;
|
||||
}
|
||||
case this.backButton: {
|
||||
let currentState = this.requestStore.getState();
|
||||
let {
|
||||
page,
|
||||
request,
|
||||
"shipping-address-page": shippingAddressPage,
|
||||
"billing-address-page": billingAddressPage,
|
||||
"basic-card-page": basicCardPage,
|
||||
selectedShippingAddress,
|
||||
} = currentState;
|
||||
|
||||
let nextState = {
|
||||
page: {
|
||||
id: page.previousId || "payment-summary",
|
||||
onboardingWizard: page.onboardingWizard,
|
||||
},
|
||||
};
|
||||
|
||||
if (page.onboardingWizard) {
|
||||
if (request.paymentOptions.requestShipping) {
|
||||
shippingAddressPage = Object.assign({}, shippingAddressPage, {
|
||||
guid: selectedShippingAddress,
|
||||
});
|
||||
Object.assign(nextState, {
|
||||
"shipping-address-page": shippingAddressPage,
|
||||
});
|
||||
} else {
|
||||
billingAddressPage = Object.assign({}, billingAddressPage, {
|
||||
guid: basicCardPage.billingAddressGUID,
|
||||
});
|
||||
Object.assign(nextState, {
|
||||
"billing-address-page": billingAddressPage,
|
||||
});
|
||||
}
|
||||
|
||||
let basicCardPageState = Object.assign({}, basicCardPage, {
|
||||
preserveFieldValues: true,
|
||||
});
|
||||
delete basicCardPageState.persistCheckboxValue;
|
||||
|
||||
Object.assign(nextState, {
|
||||
"basic-card-page": basicCardPageState,
|
||||
});
|
||||
}
|
||||
|
||||
this.requestStore.setState(nextState);
|
||||
break;
|
||||
}
|
||||
case this.saveButton: {
|
||||
if (this.form.checkValidity()) {
|
||||
this.saveRecord();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unexpected click target");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
event.target.setCustomValidity("");
|
||||
this.updateSaveButtonState();
|
||||
}
|
||||
|
||||
onInvalid(event) {
|
||||
if (event.target instanceof HTMLFormElement) {
|
||||
this.onInvalidForm(event);
|
||||
} else {
|
||||
this.onInvalidField(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event - "invalid" event
|
||||
* Note: Keep this in-sync with the equivalent version in address-form.js
|
||||
*/
|
||||
onInvalidField(event) {
|
||||
let field = event.target;
|
||||
let container = field.closest(`#${field.id}-container`);
|
||||
let errorTextSpan = paymentRequest.maybeCreateFieldErrorElement(container);
|
||||
errorTextSpan.textContent = field.validationMessage;
|
||||
}
|
||||
|
||||
onInvalidForm() {
|
||||
this.saveButton.disabled = true;
|
||||
}
|
||||
|
||||
updateSaveButtonState() {
|
||||
const INVALID_CLASS_NAME = "invalid-selected-option";
|
||||
let isValid =
|
||||
this.form.checkValidity() &&
|
||||
!this.billingAddressPicker.classList.contains(INVALID_CLASS_NAME);
|
||||
this.saveButton.disabled = !isValid;
|
||||
}
|
||||
|
||||
updateRequiredState() {
|
||||
for (let field of this.form.elements) {
|
||||
let container = field.closest(".container");
|
||||
let span = container.querySelector(".label-text");
|
||||
if (!span) {
|
||||
// The billing address field doesn't use a label inside the field.
|
||||
continue;
|
||||
}
|
||||
span.setAttribute(
|
||||
"fieldRequiredSymbol",
|
||||
this.dataset.fieldRequiredSymbol
|
||||
);
|
||||
container.toggleAttribute("required", field.required && !field.disabled);
|
||||
}
|
||||
}
|
||||
|
||||
async saveRecord() {
|
||||
let record = this.formHandler.buildFormObject();
|
||||
let currentState = this.requestStore.getState();
|
||||
let { tempBasicCards, "basic-card-page": basicCardPage } = currentState;
|
||||
let editing = !!basicCardPage.guid;
|
||||
|
||||
if (
|
||||
editing
|
||||
? basicCardPage.guid in tempBasicCards
|
||||
: !this.persistCheckbox.checked
|
||||
) {
|
||||
record.isTemporary = true;
|
||||
}
|
||||
|
||||
for (let editableFieldName of [
|
||||
"cc-name",
|
||||
"cc-exp-month",
|
||||
"cc-exp-year",
|
||||
"cc-type",
|
||||
]) {
|
||||
record[editableFieldName] = record[editableFieldName] || "";
|
||||
}
|
||||
|
||||
// Only save the card number if we're saving a new record, otherwise we'd
|
||||
// overwrite the unmasked card number with the masked one.
|
||||
if (!editing) {
|
||||
record["cc-number"] = record["cc-number"] || "";
|
||||
}
|
||||
|
||||
// Never save the CSC in storage. Storage will throw and not save the record
|
||||
// if it is passed.
|
||||
delete record["cc-csc"];
|
||||
|
||||
try {
|
||||
let { guid } = await paymentRequest.updateAutofillRecord(
|
||||
"creditCards",
|
||||
record,
|
||||
basicCardPage.guid
|
||||
);
|
||||
let { selectedStateKey } = currentState["basic-card-page"];
|
||||
if (!selectedStateKey) {
|
||||
throw new Error(
|
||||
`state["basic-card-page"].selectedStateKey is required`
|
||||
);
|
||||
}
|
||||
this.requestStore.setState({
|
||||
page: {
|
||||
id: "payment-summary",
|
||||
},
|
||||
[selectedStateKey]: guid,
|
||||
[selectedStateKey + "SecurityCode"]: this.cscInput.value,
|
||||
});
|
||||
} catch (ex) {
|
||||
log.warn("saveRecord: error:", ex);
|
||||
this.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
error: this.dataset.errorGenericSave,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("basic-card-form", BasicCardForm);
|
@ -1,33 +0,0 @@
|
||||
/* 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 AddressPicker from "./address-picker.js";
|
||||
/* import-globals-from ../unprivileged-fallbacks.js */
|
||||
|
||||
/**
|
||||
* <billing-address-picker></billing-address-picker>
|
||||
* Extends AddressPicker to treat the <select>'s value as the source of truth
|
||||
*/
|
||||
|
||||
export default class BillingAddressPicker extends AddressPicker {
|
||||
constructor() {
|
||||
super();
|
||||
this._allowEmptyOption = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object?} state - See `PaymentsStore.setState`
|
||||
* The value of the picker is the child dropdown element's value
|
||||
* @returns {string} guid
|
||||
*/
|
||||
getCurrentValue() {
|
||||
return this.dropdown.value;
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
this.render(this.requestStore.getState());
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("billing-address-picker", BillingAddressPicker);
|
@ -1,114 +0,0 @@
|
||||
/* 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 HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
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 HandleEventMixin(
|
||||
PaymentStateSubscriberMixin(PaymentRequestPage)
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.classList.add("error-page");
|
||||
this.suggestionHeading = document.createElement("p");
|
||||
this.body.append(this.suggestionHeading);
|
||||
this.suggestionsList = document.createElement("ul");
|
||||
this.suggestions = [];
|
||||
this.body.append(this.suggestionsList);
|
||||
|
||||
this.brandingSpan = document.createElement("span");
|
||||
this.brandingSpan.classList.add("branding");
|
||||
this.footer.appendChild(this.brandingSpan);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
let { request } = this.requestStore.getState();
|
||||
let { displayHost } = request.topLevelPrincipal.URI;
|
||||
for (let key of [
|
||||
"pageTitle",
|
||||
"suggestion-heading",
|
||||
"suggestion-1",
|
||||
"suggestion-2",
|
||||
"suggestion-3",
|
||||
]) {
|
||||
if (this.dataset[key] && displayHost) {
|
||||
this.dataset[key] = this.dataset[key].replace(
|
||||
"**host-name**",
|
||||
displayHost
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
this.pageTitleHeading.textContent = this.dataset.pageTitle;
|
||||
this.suggestionHeading.textContent = this.dataset.suggestionHeading;
|
||||
this.brandingSpan.textContent = this.dataset.brandingLabel;
|
||||
this.doneButton.textContent = this.dataset.doneButtonLabel;
|
||||
|
||||
this.suggestionsList.textContent = "";
|
||||
if (this.dataset["suggestion-1"]) {
|
||||
this.suggestions[0] = this.dataset["suggestion-1"];
|
||||
}
|
||||
if (this.dataset["suggestion-2"]) {
|
||||
this.suggestions[1] = this.dataset["suggestion-2"];
|
||||
}
|
||||
if (this.dataset["suggestion-3"]) {
|
||||
this.suggestions[2] = this.dataset["suggestion-3"];
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
onClick(event) {
|
||||
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);
|
@ -1,27 +0,0 @@
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="46" height="27" version="1.1">
|
||||
<defs>
|
||||
<circle id="a" cx="10" cy="10" r="10"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
|
||||
<path fill="#A1C6FF" d="M37 6.2a10.046 10.046 0 0 0 -2 -0.2c-5.523 0 -10 4.477 -10 10a9.983 9.983 0 0 0 3.999 8h-27.999a1 1 0 0 1 -1 -1v-22a1 1 0 0 1 1 -1h35a1 1 0 0 1 1 1v5.2zm-18 7.8c3.314 0 6 -1.567 6 -3.5s-2.686 -3.5 -6 -3.5 -6 1.567 -6 3.5 2.686 3.5 6 3.5z"/>
|
||||
<path fill="#5F5F5F" d="M2 17h9v2h-9v-2zm0 -15h33v3h-33v-3zm0 18h15v2h-15v-2zm10 -3h13v2h-13v-2z"/>
|
||||
<g transform="translate(25 6)">
|
||||
<mask id="b" fill="#fff">
|
||||
<use xlink:href="#a"/>
|
||||
</mask>
|
||||
<use stroke="#FFF" stroke-width="1.5" xlink:href="#a"/>
|
||||
<g mask="url(#b)">
|
||||
<g transform="translate(-77 -31)">
|
||||
<rect width="99.39" height="69.141" x="0" y="0" fill="#A1C6FF" fill-rule="evenodd" rx="1"/>
|
||||
<path fill="#5F5F5F" fill-rule="evenodd" d="M79 46h17v6h-17z"/>
|
||||
<text fill="none" font-family="sans-serif" font-size="6">
|
||||
<tspan x="80" y="42" fill="#5F5F5F">1234</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
@ -1,25 +0,0 @@
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="45" height="27" version="1.1">
|
||||
<defs>
|
||||
<circle id="a" cx="10" cy="10" r="10"/>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd" stroke="none" stroke-width="1">
|
||||
<path fill="#62A0FF" d="M37 6.458a9.996 9.996 0 0 0 -3 -0.458c-3.701 0 -6.933 2.011 -8.662 5h-22.338v5h21a9.983 9.983 0 0 0 3.999 8h-26.999a1 1 0 0 1 -1 -1v-22a1 1 0 0 1 1 -1h35a1 1 0 0 1 1 1v5.458z"/>
|
||||
<path fill="#5F5F5F" d="M37 6.458a9.996 9.996 0 0 0 -3 -0.458 9.97 9.97 0 0 0 -7.141 3h-26.859v-6h37v3.458z"/>
|
||||
<g transform="translate(24 6)">
|
||||
<mask id="b" fill="#fff">
|
||||
<use xlink:href="#a"/>
|
||||
</mask>
|
||||
<use stroke="#FFF" stroke-width="1.5" xlink:href="#a"/>
|
||||
<g mask="url(#b)">
|
||||
<path fill="#62A0FF" fill-rule="evenodd" d="M-41.923 -15.615h64.476a1 1 0 0 1 1 1v44.244a1 1 0 0 1 -1 1h-64.476a1 1 0 0 1 -1 -1v-44.244a1 1 0 0 1 1 -1zm2.923 19.615v9h55v-9h-55z"/>
|
||||
<path fill="#5F5F5F" fill-rule="evenodd" d="M-43 -10h66v12h-66z"/>
|
||||
<text fill="none" font-family="sans-serif" font-size="6" transform="translate(-43.923 -15.615)">
|
||||
<tspan x="47.676" y="26.104" fill="#5F5F5F">123</tspan>
|
||||
</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.5 KiB |
@ -1,38 +0,0 @@
|
||||
.error-page.illustrated > .page-body {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 160px;
|
||||
background-position: left center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 160px;
|
||||
padding-inline-start: 160px;
|
||||
}
|
||||
|
||||
.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;
|
||||
font-weight: lighter;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.error-page.illustrated > .page-body > p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.error-page.illustrated > .page-body > ul {
|
||||
margin-top: .5rem;
|
||||
}
|
||||
|
||||
.error-page#completion-timeout-error > .page-body {
|
||||
background-image: url("./timeout.svg");
|
||||
}
|
||||
|
||||
.error-page#completion-fail-error > .page-body {
|
||||
background-image: url("./warning.svg");
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
order-details {
|
||||
display: grid;
|
||||
grid-template-columns: 20% auto 10rem;
|
||||
grid-gap: 1em;
|
||||
margin: 1px 4vw;
|
||||
}
|
||||
|
||||
order-details > ul {
|
||||
list-style-type: none;
|
||||
margin: 1em 0;
|
||||
padding: 0;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
order-details payment-details-item {
|
||||
margin: 1px 0;
|
||||
display: contents;
|
||||
}
|
||||
payment-details-item .label {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
payment-details-item currency-amount {
|
||||
grid-column-start: 3;
|
||||
grid-column-end: 4;
|
||||
}
|
||||
|
||||
order-details .footer-items-list:not(:empty):before {
|
||||
border: 1px solid GrayText;
|
||||
display: block;
|
||||
content: "";
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 4;
|
||||
}
|
||||
|
||||
order-details > .details-total {
|
||||
margin: 1px 0;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.details-total > .label {
|
||||
margin: 0;
|
||||
font-size: large;
|
||||
grid-column-start: 2;
|
||||
grid-column-end: 3;
|
||||
text-align: end;
|
||||
}
|
||||
.details-total > currency-amount {
|
||||
font-size: large;
|
||||
text-align: end;
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
// <currency-amount> is used in the <template>
|
||||
import "../components/currency-amount.js";
|
||||
import PaymentDetailsItem from "../components/payment-details-item.js";
|
||||
import paymentRequest from "../paymentRequest.js";
|
||||
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
|
||||
|
||||
/**
|
||||
* <order-details></order-details>
|
||||
*/
|
||||
|
||||
export default class OrderDetails extends PaymentStateSubscriberMixin(
|
||||
HTMLElement
|
||||
) {
|
||||
connectedCallback() {
|
||||
if (!this._contents) {
|
||||
let template = document.getElementById("order-details-template");
|
||||
let contents = (this._contents = document.importNode(
|
||||
template.content,
|
||||
true
|
||||
));
|
||||
|
||||
this._mainItemsList = contents.querySelector(".main-list");
|
||||
this._footerItemsList = contents.querySelector(".footer-items-list");
|
||||
this._totalAmount = contents.querySelector(
|
||||
".details-total > currency-amount"
|
||||
);
|
||||
|
||||
this.appendChild(this._contents);
|
||||
}
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
get mainItemsList() {
|
||||
return this._mainItemsList;
|
||||
}
|
||||
|
||||
get footerItemsList() {
|
||||
return this._footerItemsList;
|
||||
}
|
||||
|
||||
get totalAmountElem() {
|
||||
return this._totalAmount;
|
||||
}
|
||||
|
||||
static _emptyList(listEl) {
|
||||
while (listEl.lastChild) {
|
||||
listEl.removeChild(listEl.lastChild);
|
||||
}
|
||||
}
|
||||
|
||||
static _populateList(listEl, items) {
|
||||
let fragment = document.createDocumentFragment();
|
||||
for (let item of items) {
|
||||
let row = new PaymentDetailsItem();
|
||||
row.label = item.label;
|
||||
row.amountValue = item.amount.value;
|
||||
row.amountCurrency = item.amount.currency;
|
||||
fragment.appendChild(row);
|
||||
}
|
||||
listEl.appendChild(fragment);
|
||||
return listEl;
|
||||
}
|
||||
|
||||
_getAdditionalDisplayItems(state) {
|
||||
let methodId = state.selectedPaymentCard;
|
||||
let modifier = paymentRequest.getModifierForPaymentMethod(state, methodId);
|
||||
if (modifier && modifier.additionalDisplayItems) {
|
||||
return modifier.additionalDisplayItems;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
render(state) {
|
||||
let totalItem = paymentRequest.getTotalItem(state);
|
||||
|
||||
OrderDetails._emptyList(this.mainItemsList);
|
||||
OrderDetails._emptyList(this.footerItemsList);
|
||||
|
||||
let mainItems = OrderDetails._getMainListItems(state);
|
||||
if (mainItems.length) {
|
||||
OrderDetails._populateList(this.mainItemsList, mainItems);
|
||||
}
|
||||
|
||||
let footerItems = OrderDetails._getFooterListItems(state);
|
||||
if (footerItems.length) {
|
||||
OrderDetails._populateList(this.footerItemsList, footerItems);
|
||||
}
|
||||
|
||||
this.totalAmountElem.value = totalItem.amount.value;
|
||||
this.totalAmountElem.currency = totalItem.amount.currency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a display item should belong in the footer list.
|
||||
* This uses the proposed "type" property, tracked at:
|
||||
* https://github.com/w3c/payment-request/issues/163
|
||||
*
|
||||
* @param {object} item - Data representing a PaymentItem
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isFooterItem(item) {
|
||||
return item.type == "tax";
|
||||
}
|
||||
|
||||
static _getMainListItems(state) {
|
||||
let request = state.request;
|
||||
let items = request.paymentDetails.displayItems;
|
||||
if (Array.isArray(items) && items.length) {
|
||||
let predicate = item => !OrderDetails.isFooterItem(item);
|
||||
return request.paymentDetails.displayItems.filter(predicate);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
static _getFooterListItems(state) {
|
||||
let request = state.request;
|
||||
let items = request.paymentDetails.displayItems;
|
||||
let footerItems = [];
|
||||
let methodId = state.selectedPaymentCard;
|
||||
if (methodId) {
|
||||
let modifier = paymentRequest.getModifierForPaymentMethod(
|
||||
state,
|
||||
methodId
|
||||
);
|
||||
if (modifier && Array.isArray(modifier.additionalDisplayItems)) {
|
||||
footerItems.push(...modifier.additionalDisplayItems);
|
||||
}
|
||||
}
|
||||
if (Array.isArray(items) && items.length) {
|
||||
let predicate = OrderDetails.isFooterItem;
|
||||
footerItems.push(
|
||||
...request.paymentDetails.displayItems.filter(predicate)
|
||||
);
|
||||
}
|
||||
return footerItems;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("order-details", OrderDetails);
|
@ -1,593 +0,0 @@
|
||||
/* 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 HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
import PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
|
||||
import paymentRequest from "../paymentRequest.js";
|
||||
|
||||
import "../components/currency-amount.js";
|
||||
import "../components/payment-request-page.js";
|
||||
import "../components/accepted-cards.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>
|
||||
*
|
||||
* Warning: Do not import this module from any other module as it will import
|
||||
* everything else (see above) and ruin element independence. This can stop
|
||||
* being exported once tests stop depending on it.
|
||||
*/
|
||||
|
||||
export default class PaymentDialog extends HandleEventMixin(
|
||||
PaymentStateSubscriberMixin(HTMLElement)
|
||||
) {
|
||||
constructor() {
|
||||
super();
|
||||
this._template = document.getElementById("payment-dialog-template");
|
||||
this._cachedState = {};
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
let contents = document.importNode(this._template.content, true);
|
||||
this._hostNameEl = contents.querySelector("#host-name");
|
||||
|
||||
this._cancelButton = contents.querySelector("#cancel");
|
||||
this._cancelButton.addEventListener("click", this.cancelRequest);
|
||||
|
||||
this._payButton = contents.querySelector("#pay");
|
||||
this._payButton.addEventListener("click", this);
|
||||
|
||||
this._viewAllButton = contents.querySelector("#view-all");
|
||||
this._viewAllButton.addEventListener("click", this);
|
||||
|
||||
this._mainContainer = contents.getElementById("main-container");
|
||||
this._orderDetailsOverlay = contents.querySelector(
|
||||
"#order-details-overlay"
|
||||
);
|
||||
|
||||
this._shippingAddressPicker = contents.querySelector(
|
||||
"address-picker.shipping-related"
|
||||
);
|
||||
this._shippingOptionPicker = contents.querySelector(
|
||||
"shipping-option-picker"
|
||||
);
|
||||
this._shippingRelatedEls = contents.querySelectorAll(".shipping-related");
|
||||
this._payerRelatedEls = contents.querySelectorAll(".payer-related");
|
||||
this._payerAddressPicker = contents.querySelector(
|
||||
"address-picker.payer-related"
|
||||
);
|
||||
this._paymentMethodPicker = contents.querySelector("payment-method-picker");
|
||||
this._acceptedCardsList = contents.querySelector("accepted-cards");
|
||||
this._manageText = contents.querySelector(".manage-text");
|
||||
this._manageText.addEventListener("click", this);
|
||||
|
||||
this._header = contents.querySelector("header");
|
||||
|
||||
this._errorText = contents.querySelector("header > .page-error");
|
||||
|
||||
this._disabledOverlay = contents.getElementById("disabled-overlay");
|
||||
|
||||
this.appendChild(contents);
|
||||
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._cancelButton.removeEventListener("click", this.cancelRequest);
|
||||
this._payButton.removeEventListener("click", this.pay);
|
||||
this._viewAllButton.removeEventListener("click", this);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
onClick(event) {
|
||||
switch (event.currentTarget) {
|
||||
case this._viewAllButton:
|
||||
let orderDetailsShowing = !this.requestStore.getState()
|
||||
.orderDetailsShowing;
|
||||
this.requestStore.setState({ orderDetailsShowing });
|
||||
break;
|
||||
case this._payButton:
|
||||
this.pay();
|
||||
break;
|
||||
case this._manageText:
|
||||
if (event.target instanceof HTMLAnchorElement) {
|
||||
this.openPreferences(event);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
openPreferences(event) {
|
||||
paymentRequest.openPreferences();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
cancelRequest() {
|
||||
paymentRequest.cancel();
|
||||
}
|
||||
|
||||
pay() {
|
||||
let state = this.requestStore.getState();
|
||||
let {
|
||||
selectedPayerAddress,
|
||||
selectedPaymentCard,
|
||||
selectedPaymentCardSecurityCode,
|
||||
selectedShippingAddress,
|
||||
} = state;
|
||||
|
||||
let data = {
|
||||
selectedPaymentCardGUID: selectedPaymentCard,
|
||||
selectedPaymentCardSecurityCode,
|
||||
};
|
||||
|
||||
data.selectedShippingAddressGUID = state.request.paymentOptions
|
||||
.requestShipping
|
||||
? selectedShippingAddress
|
||||
: null;
|
||||
|
||||
data.selectedPayerAddressGUID = this._isPayerRequested(
|
||||
state.request.paymentOptions
|
||||
)
|
||||
? selectedPayerAddress
|
||||
: null;
|
||||
|
||||
paymentRequest.pay(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the selectedShippingAddress or its properties are changed.
|
||||
* @param {string} shippingAddressGUID
|
||||
*/
|
||||
changeShippingAddress(shippingAddressGUID) {
|
||||
// Clear shipping address merchant errors when the shipping address changes.
|
||||
let request = Object.assign({}, this.requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign({}, request.paymentDetails);
|
||||
request.paymentDetails.shippingAddressErrors = {};
|
||||
this.requestStore.setState({ request });
|
||||
|
||||
paymentRequest.changeShippingAddress({
|
||||
shippingAddressGUID,
|
||||
});
|
||||
}
|
||||
|
||||
changeShippingOption(optionID) {
|
||||
paymentRequest.changeShippingOption({
|
||||
optionID,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the selectedPaymentCard or its relevant properties or billingAddress are changed.
|
||||
* @param {string} selectedPaymentCardBillingAddressGUID
|
||||
*/
|
||||
changePaymentMethod(selectedPaymentCardBillingAddressGUID) {
|
||||
// Clear paymentMethod merchant errors when the paymentMethod or billingAddress changes.
|
||||
let request = Object.assign({}, this.requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign({}, request.paymentDetails);
|
||||
request.paymentDetails.paymentMethodErrors = null;
|
||||
this.requestStore.setState({ request });
|
||||
|
||||
paymentRequest.changePaymentMethod({
|
||||
selectedPaymentCardBillingAddressGUID,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the selectedPayerAddress or its relevant properties are changed.
|
||||
* @param {string} payerAddressGUID
|
||||
*/
|
||||
changePayerAddress(payerAddressGUID) {
|
||||
// Clear payer address merchant errors when the payer address changes.
|
||||
let request = Object.assign({}, this.requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign({}, request.paymentDetails);
|
||||
request.paymentDetails.payerErrors = {};
|
||||
this.requestStore.setState({ request });
|
||||
|
||||
paymentRequest.changePayerAddress({
|
||||
payerAddressGUID,
|
||||
});
|
||||
}
|
||||
|
||||
_isPayerRequested(paymentOptions) {
|
||||
return (
|
||||
paymentOptions.requestPayerName ||
|
||||
paymentOptions.requestPayerEmail ||
|
||||
paymentOptions.requestPayerPhone
|
||||
);
|
||||
}
|
||||
|
||||
_getAdditionalDisplayItems(state) {
|
||||
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;
|
||||
case "": {
|
||||
// When we get a DOM update for an updateWith() or retry() the completeStatus
|
||||
// is "" when we need to show non-final screens. Don't set the page as we
|
||||
// may be on a form instead of payment-summary
|
||||
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`
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
async setStateFromParent(state) {
|
||||
let oldAddresses = paymentRequest.getAddresses(
|
||||
this.requestStore.getState()
|
||||
);
|
||||
let oldBasicCards = paymentRequest.getBasicCards(
|
||||
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,
|
||||
selectedShippingOption,
|
||||
} = state;
|
||||
let addresses = paymentRequest.getAddresses(state);
|
||||
let { paymentOptions } = state.request;
|
||||
|
||||
if (paymentOptions.requestShipping) {
|
||||
let shippingOptions = state.request.paymentDetails.shippingOptions;
|
||||
let shippingAddress =
|
||||
selectedShippingAddress && addresses[selectedShippingAddress];
|
||||
let oldShippingAddress =
|
||||
selectedShippingAddress && oldAddresses[selectedShippingAddress];
|
||||
|
||||
// Ensure `selectedShippingAddress` never refers to a deleted address.
|
||||
// We also compare address timestamps to notify about changes
|
||||
// made outside the payments UI.
|
||||
if (shippingAddress) {
|
||||
// invalidate the cached value if the address was modified
|
||||
if (
|
||||
oldShippingAddress &&
|
||||
shippingAddress.guid == oldShippingAddress.guid &&
|
||||
shippingAddress.timeLastModified !=
|
||||
oldShippingAddress.timeLastModified
|
||||
) {
|
||||
delete this._cachedState.selectedShippingAddress;
|
||||
}
|
||||
} else if (selectedShippingAddress !== null) {
|
||||
// null out the `selectedShippingAddress` property if it is undefined,
|
||||
// or if the address it pointed to was removed from storage.
|
||||
log.debug("resetting invalid/deleted shipping address");
|
||||
this.requestStore.setState({
|
||||
selectedShippingAddress: null,
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure `selectedShippingOption` never refers to a deleted shipping option and
|
||||
// matches the merchant's selected option if the user hasn't made a choice.
|
||||
if (
|
||||
shippingOptions &&
|
||||
(!selectedShippingOption ||
|
||||
!shippingOptions.find(opt => opt.id == selectedShippingOption))
|
||||
) {
|
||||
this._cachedState.selectedShippingOption = selectedShippingOption;
|
||||
this.requestStore.setState({
|
||||
// Use the DOM's computed selected shipping option:
|
||||
selectedShippingOption: state.request.shippingOption,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let basicCards = paymentRequest.getBasicCards(state);
|
||||
let oldPaymentMethod =
|
||||
selectedPaymentCard && oldBasicCards[selectedPaymentCard];
|
||||
let paymentMethod = selectedPaymentCard && basicCards[selectedPaymentCard];
|
||||
if (
|
||||
oldPaymentMethod &&
|
||||
paymentMethod.guid == oldPaymentMethod.guid &&
|
||||
paymentMethod.timeLastModified != oldPaymentMethod.timeLastModified
|
||||
) {
|
||||
delete this._cachedState.selectedPaymentCard;
|
||||
} else {
|
||||
// Changes to the billing address record don't change the `timeLastModified`
|
||||
// on the card record so we have to check for changes to the address separately.
|
||||
|
||||
let billingAddressGUID =
|
||||
paymentMethod && paymentMethod.billingAddressGUID;
|
||||
let billingAddress = billingAddressGUID && addresses[billingAddressGUID];
|
||||
let oldBillingAddress =
|
||||
billingAddressGUID && oldAddresses[billingAddressGUID];
|
||||
|
||||
if (
|
||||
oldBillingAddress &&
|
||||
billingAddress &&
|
||||
billingAddress.timeLastModified != oldBillingAddress.timeLastModified
|
||||
) {
|
||||
delete this._cachedState.selectedPaymentCard;
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `selectedPaymentCard` never refers to a deleted payment card.
|
||||
if (selectedPaymentCard && !basicCards[selectedPaymentCard]) {
|
||||
this.requestStore.setState({
|
||||
selectedPaymentCard: null,
|
||||
selectedPaymentCardSecurityCode: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (this._isPayerRequested(state.request.paymentOptions)) {
|
||||
let payerAddress =
|
||||
selectedPayerAddress && addresses[selectedPayerAddress];
|
||||
let oldPayerAddress =
|
||||
selectedPayerAddress && oldAddresses[selectedPayerAddress];
|
||||
|
||||
if (
|
||||
oldPayerAddress &&
|
||||
payerAddress &&
|
||||
((paymentOptions.requestPayerName &&
|
||||
payerAddress.name != oldPayerAddress.name) ||
|
||||
(paymentOptions.requestPayerEmail &&
|
||||
payerAddress.email != oldPayerAddress.email) ||
|
||||
(paymentOptions.requestPayerPhone &&
|
||||
payerAddress.tel != oldPayerAddress.tel))
|
||||
) {
|
||||
// invalidate the cached value if the payer address fields were modified
|
||||
delete this._cachedState.selectedPayerAddress;
|
||||
}
|
||||
|
||||
// Ensure `selectedPayerAddress` never refers to a deleted address and refers
|
||||
// to an address if one exists.
|
||||
if (!addresses[selectedPayerAddress]) {
|
||||
this.requestStore.setState({
|
||||
selectedPayerAddress: Object.keys(addresses)[0] || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_renderPayButton(state) {
|
||||
let completeStatus = state.request.completeStatus;
|
||||
switch (completeStatus) {
|
||||
case "processing":
|
||||
case "success":
|
||||
case "unknown": {
|
||||
this._payButton.disabled = true;
|
||||
this._payButton.textContent = this._payButton.dataset[
|
||||
completeStatus + "Label"
|
||||
];
|
||||
break;
|
||||
}
|
||||
case "": {
|
||||
// initial/default state
|
||||
this._payButton.textContent = this._payButton.dataset.label;
|
||||
const INVALID_CLASS_NAME = "invalid-selected-option";
|
||||
this._payButton.disabled =
|
||||
(state.request.paymentOptions.requestShipping &&
|
||||
(!this._shippingAddressPicker.selectedOption ||
|
||||
this._shippingAddressPicker.classList.contains(
|
||||
INVALID_CLASS_NAME
|
||||
) ||
|
||||
!this._shippingOptionPicker.selectedOption)) ||
|
||||
(this._isPayerRequested(state.request.paymentOptions) &&
|
||||
(!this._payerAddressPicker.selectedOption ||
|
||||
this._payerAddressPicker.classList.contains(
|
||||
INVALID_CLASS_NAME
|
||||
))) ||
|
||||
!this._paymentMethodPicker.securityCodeInput.isValid ||
|
||||
!this._paymentMethodPicker.selectedOption ||
|
||||
this._paymentMethodPicker.classList.contains(INVALID_CLASS_NAME) ||
|
||||
state.changesPrevented;
|
||||
break;
|
||||
}
|
||||
case "fail":
|
||||
case "timeout": {
|
||||
// pay button is hidden in fail/timeout states.
|
||||
this._payButton.textContent = this._payButton.dataset.label;
|
||||
this._payButton.disabled = true;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid completeStatus: ${completeStatus}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_renderPayerFields(state) {
|
||||
let paymentOptions = state.request.paymentOptions;
|
||||
let payerRequested = this._isPayerRequested(paymentOptions);
|
||||
let payerAddressForm = this.querySelector(
|
||||
"address-form[selected-state-key='selectedPayerAddress']"
|
||||
);
|
||||
|
||||
for (let element of this._payerRelatedEls) {
|
||||
element.hidden = !payerRequested;
|
||||
}
|
||||
|
||||
if (payerRequested) {
|
||||
let fieldNames = new Set();
|
||||
if (paymentOptions.requestPayerName) {
|
||||
fieldNames.add("name");
|
||||
}
|
||||
if (paymentOptions.requestPayerEmail) {
|
||||
fieldNames.add("email");
|
||||
}
|
||||
if (paymentOptions.requestPayerPhone) {
|
||||
fieldNames.add("tel");
|
||||
}
|
||||
let addressFields = [...fieldNames].join(" ");
|
||||
this._payerAddressPicker.setAttribute("address-fields", addressFields);
|
||||
if (payerAddressForm.form) {
|
||||
payerAddressForm.form.dataset.extraRequiredFields = addressFields;
|
||||
}
|
||||
|
||||
// For the payer picker we want to have a line break after the name field (#1)
|
||||
// if all three fields are requested.
|
||||
if (fieldNames.size == 3) {
|
||||
this._payerAddressPicker.setAttribute("break-after-nth-field", 1);
|
||||
} else {
|
||||
this._payerAddressPicker.removeAttribute("break-after-nth-field");
|
||||
}
|
||||
} else {
|
||||
this._payerAddressPicker.removeAttribute("address-fields");
|
||||
}
|
||||
}
|
||||
|
||||
stateChangeCallback(state) {
|
||||
super.stateChangeCallback(state);
|
||||
|
||||
// Don't dispatch change events for initial selectedShipping* changes at initialization
|
||||
// if requestShipping is false.
|
||||
if (state.request.paymentOptions.requestShipping) {
|
||||
if (
|
||||
state.selectedShippingAddress !=
|
||||
this._cachedState.selectedShippingAddress
|
||||
) {
|
||||
this.changeShippingAddress(state.selectedShippingAddress);
|
||||
}
|
||||
|
||||
if (
|
||||
state.selectedShippingOption != this._cachedState.selectedShippingOption
|
||||
) {
|
||||
this.changeShippingOption(state.selectedShippingOption);
|
||||
}
|
||||
}
|
||||
|
||||
let selectedPaymentCard = state.selectedPaymentCard;
|
||||
let basicCards = paymentRequest.getBasicCards(state);
|
||||
let billingAddressGUID = (basicCards[selectedPaymentCard] || {})
|
||||
.billingAddressGUID;
|
||||
if (
|
||||
selectedPaymentCard != this._cachedState.selectedPaymentCard &&
|
||||
billingAddressGUID
|
||||
) {
|
||||
// Update _cachedState to prevent an infinite loop when changePaymentMethod updates state.
|
||||
this._cachedState.selectedPaymentCard = state.selectedPaymentCard;
|
||||
this.changePaymentMethod(billingAddressGUID);
|
||||
}
|
||||
|
||||
if (this._isPayerRequested(state.request.paymentOptions)) {
|
||||
if (
|
||||
state.selectedPayerAddress != this._cachedState.selectedPayerAddress
|
||||
) {
|
||||
this.changePayerAddress(state.selectedPayerAddress);
|
||||
}
|
||||
}
|
||||
|
||||
this._cachedState.selectedShippingAddress = state.selectedShippingAddress;
|
||||
this._cachedState.selectedShippingOption = state.selectedShippingOption;
|
||||
this._cachedState.selectedPayerAddress = state.selectedPayerAddress;
|
||||
}
|
||||
|
||||
render(state) {
|
||||
let request = state.request;
|
||||
let paymentDetails = request.paymentDetails;
|
||||
this._hostNameEl.textContent = request.topLevelPrincipal.URI.displayHost;
|
||||
|
||||
let displayItems = request.paymentDetails.displayItems || [];
|
||||
let additionalItems = this._getAdditionalDisplayItems(state);
|
||||
this._viewAllButton.hidden =
|
||||
!displayItems.length && !additionalItems.length;
|
||||
|
||||
let shippingType = state.request.paymentOptions.shippingType || "shipping";
|
||||
let addressPickerLabel = this._shippingAddressPicker.dataset[
|
||||
shippingType + "AddressLabel"
|
||||
];
|
||||
this._shippingAddressPicker.setAttribute("label", addressPickerLabel);
|
||||
let optionPickerLabel = this._shippingOptionPicker.dataset[
|
||||
shippingType + "OptionsLabel"
|
||||
];
|
||||
this._shippingOptionPicker.setAttribute("label", optionPickerLabel);
|
||||
|
||||
let shippingAddressForm = this.querySelector(
|
||||
"address-form[selected-state-key='selectedShippingAddress']"
|
||||
);
|
||||
shippingAddressForm.dataset.titleAdd = this.dataset[
|
||||
shippingType + "AddressTitleAdd"
|
||||
];
|
||||
shippingAddressForm.dataset.titleEdit = this.dataset[
|
||||
shippingType + "AddressTitleEdit"
|
||||
];
|
||||
|
||||
let totalItem = paymentRequest.getTotalItem(state);
|
||||
let totalAmountEl = this.querySelector("#total > currency-amount");
|
||||
totalAmountEl.value = totalItem.amount.value;
|
||||
totalAmountEl.currency = totalItem.amount.currency;
|
||||
|
||||
// Show the total header on the address and basic card pages only during
|
||||
// on-boarding(FTU) and on the payment summary page.
|
||||
this._header.hidden =
|
||||
!state.page.onboardingWizard && state.page.id != "payment-summary";
|
||||
|
||||
this._orderDetailsOverlay.hidden = !state.orderDetailsShowing;
|
||||
let genericError = "";
|
||||
if (
|
||||
this._shippingAddressPicker.selectedOption &&
|
||||
(!request.paymentDetails.shippingOptions ||
|
||||
!request.paymentDetails.shippingOptions.length)
|
||||
) {
|
||||
genericError = this._errorText.dataset[shippingType + "GenericError"];
|
||||
}
|
||||
this._errorText.textContent = paymentDetails.error || genericError;
|
||||
|
||||
let paymentOptions = request.paymentOptions;
|
||||
for (let element of this._shippingRelatedEls) {
|
||||
element.hidden = !paymentOptions.requestShipping;
|
||||
}
|
||||
|
||||
this._renderPayerFields(state);
|
||||
|
||||
let isMac = /mac/i.test(navigator.platform);
|
||||
for (let manageTextEl of this._manageText.children) {
|
||||
manageTextEl.hidden = manageTextEl.dataset.os == "mac" ? !isMac : isMac;
|
||||
let link = manageTextEl.querySelector("a");
|
||||
// The href is only set to be exposed to accessibility tools so users know what will open.
|
||||
// The actual opening happens from the click event listener.
|
||||
link.href = "about:preferences#privacy-form-autofill";
|
||||
}
|
||||
|
||||
this._renderPayButton(state);
|
||||
|
||||
for (let page of this._mainContainer.querySelectorAll(":scope > .page")) {
|
||||
page.hidden = state.page.id != page.id;
|
||||
}
|
||||
|
||||
this.toggleAttribute("changes-prevented", state.changesPrevented);
|
||||
this.setAttribute("complete-status", request.completeStatus);
|
||||
this._disabledOverlay.hidden = !state.changesPrevented;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("payment-dialog", PaymentDialog);
|
@ -1,199 +0,0 @@
|
||||
/* 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 BasicCardOption from "../components/basic-card-option.js";
|
||||
import CscInput from "../components/csc-input.js";
|
||||
import HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
import RichPicker from "./rich-picker.js";
|
||||
import paymentRequest from "../paymentRequest.js";
|
||||
|
||||
/* import-globals-from ../unprivileged-fallbacks.js */
|
||||
|
||||
/**
|
||||
* <payment-method-picker></payment-method-picker>
|
||||
* Container around add/edit links and <rich-select> with
|
||||
* <basic-card-option> listening to savedBasicCards.
|
||||
*/
|
||||
|
||||
export default class PaymentMethodPicker extends HandleEventMixin(RichPicker) {
|
||||
constructor() {
|
||||
super();
|
||||
this.dropdown.setAttribute("option-type", "basic-card-option");
|
||||
this.securityCodeInput = new CscInput();
|
||||
this.securityCodeInput.className = "security-code-container";
|
||||
this.securityCodeInput.placeholder = this.dataset.cscPlaceholder;
|
||||
this.securityCodeInput.backTooltip = this.dataset.cscBackTooltip;
|
||||
this.securityCodeInput.frontTooltip = this.dataset.cscFrontTooltip;
|
||||
this.securityCodeInput.addEventListener("change", this);
|
||||
this.securityCodeInput.addEventListener("input", this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.dropdown.after(this.securityCodeInput);
|
||||
}
|
||||
|
||||
get fieldNames() {
|
||||
let fieldNames = [...BasicCardOption.recordAttributes];
|
||||
return fieldNames;
|
||||
}
|
||||
|
||||
render(state) {
|
||||
let basicCards = paymentRequest.getBasicCards(state);
|
||||
let desiredOptions = [];
|
||||
for (let [guid, basicCard] of Object.entries(basicCards)) {
|
||||
let optionEl = this.dropdown.getOptionByValue(guid);
|
||||
if (!optionEl) {
|
||||
optionEl = document.createElement("option");
|
||||
optionEl.value = guid;
|
||||
}
|
||||
|
||||
for (let key of BasicCardOption.recordAttributes) {
|
||||
let val = basicCard[key];
|
||||
if (val) {
|
||||
optionEl.setAttribute(key, val);
|
||||
} else {
|
||||
optionEl.removeAttribute(key);
|
||||
}
|
||||
}
|
||||
|
||||
optionEl.textContent = BasicCardOption.formatSingleLineLabel(basicCard);
|
||||
desiredOptions.push(optionEl);
|
||||
}
|
||||
|
||||
this.dropdown.popupBox.textContent = "";
|
||||
for (let option of desiredOptions) {
|
||||
this.dropdown.popupBox.appendChild(option);
|
||||
}
|
||||
|
||||
// Update selectedness after the options are updated
|
||||
let selectedPaymentCardGUID = state[this.selectedStateKey];
|
||||
if (selectedPaymentCardGUID) {
|
||||
this.dropdown.value = selectedPaymentCardGUID;
|
||||
|
||||
if (selectedPaymentCardGUID !== this.dropdown.value) {
|
||||
throw new Error(
|
||||
`The option ${selectedPaymentCardGUID} ` +
|
||||
`does not exist in the payment method picker`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.dropdown.value = "";
|
||||
}
|
||||
|
||||
let securityCodeState = state[this.selectedStateKey + "SecurityCode"];
|
||||
if (
|
||||
securityCodeState &&
|
||||
securityCodeState != this.securityCodeInput.value
|
||||
) {
|
||||
this.securityCodeInput.defaultValue = securityCodeState;
|
||||
}
|
||||
|
||||
let selectedCardType =
|
||||
(basicCards[selectedPaymentCardGUID] &&
|
||||
basicCards[selectedPaymentCardGUID]["cc-type"]) ||
|
||||
"";
|
||||
this.securityCodeInput.cardType = selectedCardType;
|
||||
|
||||
super.render(state);
|
||||
}
|
||||
|
||||
errorForSelectedOption(state) {
|
||||
let superError = super.errorForSelectedOption(state);
|
||||
if (superError) {
|
||||
return superError;
|
||||
}
|
||||
let selectedOption = this.selectedOption;
|
||||
if (!selectedOption) {
|
||||
return "";
|
||||
}
|
||||
|
||||
let basicCardMethod = state.request.paymentMethods.find(
|
||||
method => method.supportedMethods == "basic-card"
|
||||
);
|
||||
let merchantNetworks =
|
||||
basicCardMethod &&
|
||||
basicCardMethod.data &&
|
||||
basicCardMethod.data.supportedNetworks;
|
||||
let acceptedNetworks =
|
||||
merchantNetworks || PaymentDialogUtils.getCreditCardNetworks();
|
||||
let selectedCard = paymentRequest.getBasicCards(state)[
|
||||
selectedOption.value
|
||||
];
|
||||
let isSupported =
|
||||
selectedCard["cc-type"] &&
|
||||
acceptedNetworks.includes(selectedCard["cc-type"]);
|
||||
return isSupported ? "" : this.dataset.invalidLabel;
|
||||
}
|
||||
|
||||
get selectedStateKey() {
|
||||
return this.getAttribute("selected-state-key");
|
||||
}
|
||||
|
||||
onInput(event) {
|
||||
this.onInputOrChange(event);
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
this.onInputOrChange(event);
|
||||
}
|
||||
|
||||
onInputOrChange({ currentTarget }) {
|
||||
let selectedKey = this.selectedStateKey;
|
||||
let stateChange = {};
|
||||
|
||||
if (!selectedKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (currentTarget) {
|
||||
case this.dropdown: {
|
||||
stateChange[selectedKey] = this.dropdown.value;
|
||||
break;
|
||||
}
|
||||
case this.securityCodeInput: {
|
||||
stateChange[
|
||||
selectedKey + "SecurityCode"
|
||||
] = this.securityCodeInput.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.requestStore.setState(stateChange);
|
||||
}
|
||||
|
||||
onClick({ target }) {
|
||||
let nextState = {
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
selectedStateKey: this.selectedStateKey,
|
||||
},
|
||||
};
|
||||
|
||||
switch (target) {
|
||||
case this.addLink: {
|
||||
nextState["basic-card-page"].guid = null;
|
||||
break;
|
||||
}
|
||||
case this.editLink: {
|
||||
let state = this.requestStore.getState();
|
||||
let selectedPaymentCardGUID = state[this.selectedStateKey];
|
||||
nextState["basic-card-page"].guid = selectedPaymentCardGUID;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unexpected onClick");
|
||||
}
|
||||
}
|
||||
|
||||
this.requestStore.setState(nextState);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("payment-method-picker", PaymentMethodPicker);
|
@ -1,83 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
.rich-picker {
|
||||
display: grid;
|
||||
grid-template-columns: 5fr auto auto;
|
||||
grid-template-areas:
|
||||
"label edit add"
|
||||
"dropdown dropdown dropdown"
|
||||
"invalid invalid invalid";
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.rich-picker > label {
|
||||
color: #0c0c0d;
|
||||
font-weight: 700;
|
||||
grid-area: label;
|
||||
}
|
||||
|
||||
.rich-picker > .add-link,
|
||||
.rich-picker > .edit-link {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.rich-picker > .add-link {
|
||||
grid-area: add;
|
||||
}
|
||||
|
||||
.rich-picker > .edit-link {
|
||||
grid-area: edit;
|
||||
border-inline-end: 1px solid #0C0C0D33;
|
||||
}
|
||||
|
||||
.rich-picker > rich-select {
|
||||
grid-area: dropdown;
|
||||
}
|
||||
|
||||
.invalid-selected-option > rich-select > select {
|
||||
border: 1px solid #c70011;
|
||||
}
|
||||
|
||||
.rich-picker > .invalid-label {
|
||||
grid-area: invalid;
|
||||
font-weight: normal;
|
||||
color: #c70011;
|
||||
}
|
||||
|
||||
:not(.invalid-selected-option) > .invalid-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Payment Method Picker */
|
||||
payment-method-picker.rich-picker {
|
||||
grid-template-columns: 20fr 1fr auto auto;
|
||||
grid-template-areas:
|
||||
"label spacer edit add"
|
||||
"dropdown csc csc csc"
|
||||
"invalid invalid invalid invalid";
|
||||
}
|
||||
|
||||
.security-code-container {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
grid-area: csc;
|
||||
margin: 10px 0; /* Has to be same as rich-select */
|
||||
}
|
||||
|
||||
.rich-picker .security-code {
|
||||
border: 1px solid #0C0C0D33;
|
||||
/* Underlap the 1px border from common.css */
|
||||
margin-inline-start: -1px;
|
||||
flex-grow: 1;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.rich-picker .security-code:-moz-ui-invalid,
|
||||
.rich-picker .security-code:focus {
|
||||
/* So the error outline and focus ring appear above the adjacent dropdown when appropriate. */
|
||||
/* We don't want to always be on top or we will cover the error outline or focus outline from the
|
||||
dropdown. */
|
||||
z-index: 1;
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
/* 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 PaymentStateSubscriberMixin from "../mixins/PaymentStateSubscriberMixin.js";
|
||||
import RichSelect from "../components/rich-select.js";
|
||||
|
||||
export default class RichPicker extends PaymentStateSubscriberMixin(
|
||||
HTMLElement
|
||||
) {
|
||||
static get observedAttributes() {
|
||||
return ["label"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.classList.add("rich-picker");
|
||||
|
||||
this.dropdown = new RichSelect();
|
||||
this.dropdown.addEventListener("change", this);
|
||||
|
||||
this.labelElement = document.createElement("label");
|
||||
|
||||
this.addLink = document.createElement("a");
|
||||
this.addLink.className = "add-link";
|
||||
this.addLink.href = "javascript:void(0)";
|
||||
this.addLink.addEventListener("click", this);
|
||||
|
||||
this.editLink = document.createElement("a");
|
||||
this.editLink.className = "edit-link";
|
||||
this.editLink.href = "javascript:void(0)";
|
||||
this.editLink.addEventListener("click", this);
|
||||
|
||||
this.invalidLabel = document.createElement("label");
|
||||
this.invalidLabel.className = "invalid-label";
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (!this.dropdown.popupBox.id) {
|
||||
this.dropdown.popupBox.id =
|
||||
"select-" + Math.floor(Math.random() * 1000000);
|
||||
}
|
||||
this.labelElement.setAttribute("for", this.dropdown.popupBox.id);
|
||||
this.invalidLabel.setAttribute("for", this.dropdown.popupBox.id);
|
||||
|
||||
// The document order, by default, controls tab order so keep that in mind if changing this.
|
||||
this.appendChild(this.labelElement);
|
||||
this.appendChild(this.dropdown);
|
||||
this.appendChild(this.editLink);
|
||||
this.appendChild(this.addLink);
|
||||
this.appendChild(this.invalidLabel);
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name == "label") {
|
||||
this.labelElement.textContent = newValue;
|
||||
}
|
||||
}
|
||||
|
||||
render(state) {
|
||||
this.editLink.hidden = !this.dropdown.value;
|
||||
|
||||
let errorText = this.errorForSelectedOption(state);
|
||||
this.classList.toggle("invalid-selected-option", !!errorText);
|
||||
this.invalidLabel.textContent = errorText;
|
||||
this.addLink.textContent = this.dataset.addLinkLabel;
|
||||
this.editLink.textContent = this.dataset.editLinkLabel;
|
||||
}
|
||||
|
||||
get selectedOption() {
|
||||
return this.dropdown.selectedOption;
|
||||
}
|
||||
|
||||
get selectedRichOption() {
|
||||
return this.dropdown.selectedRichOption;
|
||||
}
|
||||
|
||||
get requiredFields() {
|
||||
return this.selectedOption ? this.selectedOption.requiredFields || [] : [];
|
||||
}
|
||||
|
||||
get fieldNames() {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} state Application state
|
||||
* @returns {string} Containing an error message for the picker or "" for no error.
|
||||
*/
|
||||
errorForSelectedOption(state) {
|
||||
if (!this.selectedOption) {
|
||||
return "";
|
||||
}
|
||||
if (!this.dataset.invalidLabel) {
|
||||
throw new Error("data-invalid-label is required");
|
||||
}
|
||||
return this.missingFieldsOfSelectedOption().length
|
||||
? this.dataset.invalidLabel
|
||||
: "";
|
||||
}
|
||||
|
||||
missingFieldsOfSelectedOption() {
|
||||
let selectedOption = this.selectedOption;
|
||||
if (!selectedOption) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let fieldNames = this.selectedRichOption.requiredFields || [];
|
||||
|
||||
// Return all field names that are empty or missing from the option.
|
||||
return fieldNames.filter(name => !selectedOption.getAttribute(name));
|
||||
}
|
||||
}
|
@ -1,72 +0,0 @@
|
||||
/* 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 RichPicker from "./rich-picker.js";
|
||||
import ShippingOption from "../components/shipping-option.js";
|
||||
import HandleEventMixin from "../mixins/HandleEventMixin.js";
|
||||
|
||||
/**
|
||||
* <shipping-option-picker></shipping-option-picker>
|
||||
* Container around <rich-select> with
|
||||
* <option> listening to shippingOptions.
|
||||
*/
|
||||
|
||||
export default class ShippingOptionPicker extends HandleEventMixin(RichPicker) {
|
||||
constructor() {
|
||||
super();
|
||||
this.dropdown.setAttribute("option-type", "shipping-option");
|
||||
}
|
||||
|
||||
render(state) {
|
||||
this.addLink.hidden = true;
|
||||
this.editLink.hidden = true;
|
||||
|
||||
// If requestShipping is true but paymentDetails.shippingOptions isn't defined
|
||||
// then use an empty array as a fallback.
|
||||
let shippingOptions = state.request.paymentDetails.shippingOptions || [];
|
||||
let desiredOptions = [];
|
||||
for (let option of shippingOptions) {
|
||||
let optionEl = this.dropdown.getOptionByValue(option.id);
|
||||
if (!optionEl) {
|
||||
optionEl = document.createElement("option");
|
||||
optionEl.value = option.id;
|
||||
}
|
||||
|
||||
optionEl.setAttribute("label", option.label);
|
||||
optionEl.setAttribute("amount-currency", option.amount.currency);
|
||||
optionEl.setAttribute("amount-value", option.amount.value);
|
||||
|
||||
optionEl.textContent = ShippingOption.formatSingleLineLabel(option);
|
||||
desiredOptions.push(optionEl);
|
||||
}
|
||||
|
||||
this.dropdown.popupBox.textContent = "";
|
||||
for (let option of desiredOptions) {
|
||||
this.dropdown.popupBox.appendChild(option);
|
||||
}
|
||||
|
||||
// Update selectedness after the options are updated
|
||||
let selectedShippingOption = state.selectedShippingOption;
|
||||
this.dropdown.value = selectedShippingOption;
|
||||
|
||||
if (
|
||||
selectedShippingOption &&
|
||||
selectedShippingOption !== this.dropdown.popupBox.value
|
||||
) {
|
||||
throw new Error(
|
||||
`The option ${selectedShippingOption} ` +
|
||||
`does not exist in the shipping option picker`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
onChange(event) {
|
||||
let selectedOptionId = this.dropdown.value;
|
||||
this.requestStore.setState({
|
||||
selectedShippingOption: selectedOptionId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("shipping-option-picker", ShippingOptionPicker);
|
@ -1,84 +0,0 @@
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="183" height="159" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="a" x1="-58.737%" x2="177.192%" y1="-3.847%" y2="112.985%">
|
||||
<stop offset="0%" stop-color="#CCFBFF"/>
|
||||
<stop offset="100%" stop-color="#C9E4FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x1="-62.081%" x2="144.194%" y1="-14.656%" y2="104.338%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#008EA4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="-93.784%" x2="130.325%" y1="-512.631%" y2="364.268%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="d" x1="-632.1%" x2="878.563%" y1="-1341.641%" y2="1656.303%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="e" x1="-145.225%" x2="166.502%" y1="-230.02%" y2="260.392%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="f" x1="-16.485%" x2="216.233%" y1="-373.776%" y2="1088.73%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="g" x1="-13.212%" x2="220.924%" y1="-398.815%" y2="1263.59%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="h" x1="56.524%" x2="154.312%" y1="90.974%" y2="705.12%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#008EA4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="i" x1="-2629.906%" x2="2946.182%" y1="-2629.905%" y2="2946.183%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="j" x1="-2788.189%" x2="2787.9%" y1="-2788.189%" y2="2787.9%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="k" x1="-2975.533%" x2="2600.555%" y1="-2975.533%" y2="2600.555%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="l" x1="-2968.553%" x2="2607.535%" y1="-2968.552%" y2="2607.537%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="m" x1="-47.612%" x2="165.227%" y1="-4.394%" y2="113.507%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1">
|
||||
<path fill="#EAEAEE" d="M41.548 94.855h110.678a1 1 0 1 0 0 -2h-110.678a1 1 0 0 0 0 2zm14.952 -5h27.764a0.5 0.5 0 1 0 0 -1h-27.765a0.5 0.5 0 1 0 0 1zm-35.386 8.869a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 0 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 0 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5z"/>
|
||||
<path fill="#FFF" d="M1.474 63.811h25.423s-7.955 -17.777 8.932 -20.076c15.062 -2.05 21.014 13.427 21.014 13.427s1.786 -8.93 10.743 -7.221c8.832 1.684 15.353 15.889 15.353 15.889h22.137"/>
|
||||
<path fill="#EAEAEE" d="M105.51 61.633h-6.544a0.588 0.588 0 1 1 0 -1.176h6.545a0.588 0.588 0 0 1 0 1.176zm-17.132 0h-1.176a0.588 0.588 0 1 1 0 -1.176h1.176a0.588 0.588 0 1 1 0 1.176zm-61.046 -0.712h-1.893a0.588 0.588 0 0 1 0 -1.176h1.023a24.94 24.94 0 0 1 -0.269 -0.756 0.588 0.588 0 0 1 1.115 -0.377 18.812 18.812 0 0 0 0.56 1.48 0.588 0.588 0 0 1 -0.536 0.829zm-11.305 0h-14.117a0.588 0.588 0 1 1 0 -1.176h14.117a0.588 0.588 0 0 1 0 1.176zm66.928 -0.064a0.588 0.588 0 0 1 -0.514 -0.301 44.273 44.273 0 0 0 -1.828 -2.974 0.588 0.588 0 1 1 0.977 -0.654 45.65 45.65 0 0 1 1.878 3.053 0.588 0.588 0 0 1 -0.513 0.876zm-57.304 -6.042a0.588 0.588 0 0 1 -0.582 -0.507 20.77 20.77 0 0 1 -0.134 -1.205 0.588 0.588 0 0 1 1.173 -0.094 19.39 19.39 0 0 0 0.126 1.136 0.588 0.588 0 0 1 -0.583 0.67zm30.647 -2.594a0.587 0.587 0 0 1 -0.519 -0.31 26.496 26.496 0 0 0 -0.57 -1.001 0.588 0.588 0 1 1 1.01 -0.605 27.467 27.467 0 0 1 0.596 1.048 0.588 0.588 0 0 1 -0.517 0.868zm18.676 -1.503a0.586 0.586 0 0 1 -0.383 -0.143 14.722 14.722 0 0 0 -6.68 -3.535 8.578 8.578 0 0 0 -5.628 0.61 0.588 0.588 0 1 1 -0.54 -1.046 9.747 9.747 0 0 1 6.389 -0.72 15.855 15.855 0 0 1 7.225 3.8 0.588 0.588 0 0 1 -0.383 1.034zm-22.019 -3.335a0.587 0.587 0 0 1 -0.443 -0.202 21.943 21.943 0 0 0 -2.45 -2.41 0.588 0.588 0 0 1 0.755 -0.903 23.254 23.254 0 0 1 2.582 2.54 0.588 0.588 0 0 1 -0.444 0.975zm-24.258 -3.43a0.589 0.589 0 0 1 -0.395 -1.025 14.421 14.421 0 0 1 7.882 -3.255 19.345 19.345 0 0 1 5.96 0.08 0.588 0.588 0 1 1 -0.203 1.158 18.263 18.263 0 0 0 -5.597 -0.073 13.284 13.284 0 0 0 -7.253 2.963 0.59 0.59 0 0 1 -0.394 0.152z"/>
|
||||
<path fill="#FFF" d="M106.242 66.149h-104.771a1.176 1.176 0 1 1 0 -2.353h104.771a1.176 1.176 0 0 1 0 2.353zm1.978 -51.759h14.187s-4.44 -9.92 4.985 -11.203c8.404 -1.144 11.726 7.493 11.726 7.493s0.907 -4.089 5.995 -4.03c4.756 0.055 8.38 7.914 8.568 8.867h12.353"/>
|
||||
<path fill="#EAEAEE" d="M122.823 12.8h-14.167a0.59 0.59 0 1 1 0 -1.18h14.167a0.59 0.59 0 0 1 0 1.18zm43.647 -0.184h-0.545a0.59 0.59 0 1 1 0 -1.18h0.545a0.59 0.59 0 1 1 0 1.18zm-5.267 0h-3.542a0.59 0.59 0 1 1 0 -1.18h3.542a0.59 0.59 0 1 1 0 1.18zm-21.648 -3.527a0.614 0.614 0 0 1 -0.553 -0.384l-0.088 -0.207a0.59 0.59 0 0 1 0.23 -0.74 5.483 5.483 0 0 1 5.186 -4 7.111 7.111 0 0 1 1.33 0.132 10.622 10.622 0 0 1 5.267 3.045 0.59 0.59 0 0 1 -0.822 0.848 9.484 9.484 0 0 0 -4.666 -2.733 5.935 5.935 0 0 0 -1.109 -0.111c-3.402 0 -4.166 3.527 -4.196 3.678a0.592 0.592 0 0 1 -0.579 0.472zm-16.422 -5.27a0.59 0.59 0 0 1 -0.45 -0.973 6.799 6.799 0 0 1 3.236 -2.026 0.59 0.59 0 1 1 0.351 1.127 5.632 5.632 0 0 0 -2.688 1.665 0.589 0.589 0 0 1 -0.45 0.208zm8.784 -1.987a0.601 0.601 0 0 1 -0.156 -0.02 9.055 9.055 0 0 0 -1.083 -0.225 0.59 0.59 0 0 1 -0.5 -0.668 0.6 0.6 0 0 1 0.668 -0.5 10.36 10.36 0 0 1 1.224 0.253 0.59 0.59 0 0 1 -0.153 1.16z"/>
|
||||
<path fill="#FFF" d="M166.948 16.76h-58.468a1.18 1.18 0 0 1 0 -2.36h58.468a1.18 1.18 0 0 1 0 2.36z"/>
|
||||
<ellipse cx="99.859" cy="152.41" fill="#EAEAEE" rx="30" ry="5.802"/>
|
||||
<path fill="#F9F9FA" d="M116.421 66.559c-4.126 4.126 -9.051 9.488 -9.656 16.465 0.605 6.977 5.53 12.339 9.656 16.465a26.199 26.199 0 0 1 9.126 19.9v11.242h-52.517v-11.242a26.199 26.199 0 0 1 9.126 -19.9c4.126 -4.126 9.051 -9.488 9.656 -16.465 -0.605 -6.977 -5.53 -12.339 -9.656 -16.465a26.199 26.199 0 0 1 -9.126 -19.9v-11.369h52.517v11.369a26.199 26.199 0 0 1 -9.126 19.9z"/>
|
||||
<path fill="url(#a)" d="M35.304 21.239c-3.349 4.126 -8.666 10.808 -9.157 17.785 0.491 6.977 5.808 13.659 9.157 17.785a25.765 25.765 0 0 1 7.408 18.58v11.242h-42.627v-11.242a25.765 25.765 0 0 1 7.407 -18.58c3.35 -4.126 8.667 -10.808 9.157 -17.785 -0.49 -6.977 -5.808 -13.659 -9.157 -17.785a25.765 25.765 0 0 1 -7.407 -18.58v-2.13a95.816 95.816 0 0 0 22.369 2.64c12.527 0 20.258 -2.64 20.258 -2.64v2.13a25.765 25.765 0 0 1 -7.408 18.58z" transform="translate(78 44)"/>
|
||||
<path fill="url(#b)" d="M55.548 6.222h-52.518a2.932 2.932 0 0 1 0 -5.864h52.518a2.932 2.932 0 0 1 0 5.864zm2.932 92.409a2.932 2.932 0 0 0 -2.932 -2.932h-52.518a2.932 2.932 0 0 0 0 5.863h52.518a2.932 2.932 0 0 0 2.932 -2.931z" transform="translate(70 32)"/>
|
||||
<path fill="url(#c)" d="M129.028 130.684c0.334 5 -13.767 7.667 -29.749 7.667 -15.981 0 -28.937 -3.358 -28.937 -7.5 0 -4.143 12.956 -7.5 28.937 -7.5 15.982 0 29.474 3.2 29.75 7.333z"/>
|
||||
<path fill="#F9F9FA" d="M125.654 127.755c0 2.185 -11.233 5.596 -25.792 5.596 -14.56 0 -26.932 -3.41 -26.932 -5.596 0 -2.185 11.802 -3.956 26.362 -3.956 14.56 0 26.362 1.771 26.362 3.956z"/>
|
||||
<path fill="url(#d)" d="M95.233 76.435s1.978 4.121 4.286 4.286c2.307 0.165 4.43 -4.388 4.43 -4.388l-8.716 0.102z"/>
|
||||
<path fill="url(#e)" d="M99.288 102.48c7.502 0 21.137 23.169 21.137 23.169s-1.442 3.702 -20.563 3.702c-19.122 0 -21.71 -3.702 -21.71 -3.702s13.763 -23.17 21.136 -23.17z"/>
|
||||
<path fill="url(#f)" d="M127.852 37.313c0 2.185 -12.049 5.038 -27.657 5.038s-28.864 -2.853 -28.864 -5.038 12.653 -3.956 28.26 -3.956c15.609 0 28.261 1.771 28.261 3.956z"/>
|
||||
<ellipse cx="99.42" cy="33.357" fill="url(#g)" rx="28.089" ry="3.956"/>
|
||||
<ellipse cx="99.603" cy="76.333" fill="url(#h)" rx="4.346" ry="1"/>
|
||||
<circle cx="99.393" cy="84.717" r="1.181" fill="url(#i)"/>
|
||||
<circle cx="98.996" cy="92.589" r="1.181" fill="url(#j)"/>
|
||||
<circle cx="98.145" cy="102.288" r="1.181" fill="url(#k)"/>
|
||||
<circle cx="101.388" cy="98.715" r="1.181" fill="url(#l)"/>
|
||||
<path fill="#F9F9FA" d="M84.628 53.363a1.833 1.833 0 0 1 -0.264 -0.026l-0.237 -0.08a41.18 41.18 0 0 0 -0.238 -0.118 2.238 2.238 0 0 1 -0.197 -0.172 1.325 1.325 0 0 1 -0.382 -0.922 1.359 1.359 0 0 1 0.105 -0.515 1.297 1.297 0 0 1 0.277 -0.422 1.012 1.012 0 0 1 0.197 -0.158 2.196 2.196 0 0 1 0.238 -0.132 1.55 1.55 0 0 1 0.237 -0.066 1.331 1.331 0 0 1 1.2 0.356 1.297 1.297 0 0 1 0.278 0.422 1.169 1.169 0 0 1 0.105 0.515 1.329 1.329 0 0 1 -1.319 1.318zm-0.221 69.007a1.319 1.319 0 0 1 -1.318 -1.282c-0.227 -8.439 2.93 -13.501 3.064 -13.712a1.319 1.319 0 0 1 2.227 1.413c-0.057 0.092 -2.858 4.684 -2.653 12.227a1.32 1.32 0 0 1 -1.283 1.355h-0.037zm8.793 -54.507a1.319 1.319 0 0 1 -1.002 -0.46 102.116 102.116 0 0 1 -6.712 -9.614 1.319 1.319 0 0 1 2.24 -1.39 101.694 101.694 0 0 0 6.476 9.287 1.32 1.32 0 0 1 -1.002 2.177z"/>
|
||||
<path fill="url(#m)" d="M127.377 126.244v-6.856a28.144 28.144 0 0 0 -9.656 -21.217c-4.074 -4.092 -8.47 -8.976 -9.052 -15.1 0.582 -6.218 4.978 -11.103 9.026 -15.17a28.118 28.118 0 0 0 9.682 -21.242v-6.529a3.426 3.426 0 0 0 2.27 -2.456 4.696 4.696 0 0 0 -0.888 -5.862c-3.257 -4.026 -24.92 -4.23 -29.23 -4.23 -10.758 0 -24.656 0.91 -28.356 3.436a4.777 4.777 0 0 0 -2.674 4.272 4.71 4.71 0 0 0 2.19 3.98 4.54 4.54 0 0 0 0.73 0.528v6.86a28.09 28.09 0 0 0 9.657 21.218c4.074 4.093 8.47 8.98 9.051 15.101 -0.582 6.215 -4.977 11.102 -9.025 15.169a28.149 28.149 0 0 0 -9.68 21.317l-0.003 6.78a4.741 4.741 0 0 0 -2.92 4.387 4.455 4.455 0 0 0 0.319 1.685c1.459 6.64 27.596 6.83 30.571 6.83 2.951 0 28.885 -0.189 30.526 -6.661a4.734 4.734 0 0 0 -2.538 -6.24zm0.365 5.42l-0.041 0.132c-0.48 3.01 -15.028 5.032 -28.312 5.032 -16.33 0 -28.03 -2.665 -28.315 -5.058l-0.04 -0.145a2.426 2.426 0 0 1 2.205 -3.426h0.5v-8.81a25.817 25.817 0 0 1 8.946 -19.548c4.404 -4.42 9.154 -9.727 9.763 -16.86 -0.609 -7.047 -5.359 -12.354 -9.79 -16.8a25.792 25.792 0 0 1 -8.92 -19.522v-8.348l-0.318 -0.124a3.457 3.457 0 0 1 -1.232 -0.69l-0.117 -0.091a2.422 2.422 0 0 1 0.19 -4.336l0.094 -0.056c2.392 -1.774 14.074 -3.113 27.175 -3.113 15.231 0 26.482 1.747 27.434 3.379l0.115 0.134a2.396 2.396 0 0 1 0.333 3.42l-0.165 0.2 0.094 0.32c-0.057 0.126 -0.362 0.534 -1.938 1.048l-0.345 0.112v8.145a25.817 25.817 0 0 1 -8.946 19.547c-4.404 4.421 -9.154 9.728 -9.763 16.86 0.609 7.048 5.359 12.354 9.79 16.8a25.813 25.813 0 0 1 8.92 19.564v8.769h0.5a2.42 2.42 0 0 1 2.183 3.464z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 12 KiB |
@ -1,32 +0,0 @@
|
||||
<!-- 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/. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="162" height="147" version="1.1">
|
||||
<defs>
|
||||
<linearGradient id="a" x1="-5.18%" x2="85.305%" y1="13.831%" y2="117.402%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="b" x1="-26.306%" x2="110.545%" y1="-9.375%" y2="148.477%">
|
||||
<stop offset="0%" stop-color="#CCFBFF"/>
|
||||
<stop offset="100%" stop-color="#C9E4FF"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="c" x1="-335.989%" x2="397.876%" y1="-66.454%" y2="146.763%">
|
||||
<stop offset="0%" stop-color="#00C8D7"/>
|
||||
<stop offset="100%" stop-color="#0A84FF"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1">
|
||||
<path fill="#EAEAEE" d="M20.53 83.44h110.678a1 1 0 1 0 0 -2h-110.679a1 1 0 0 0 0 2zm14.951 -5h27.765a0.5 0.5 0 1 0 0 -1h-27.765a0.5 0.5 0 1 0 0 1zm-35.385 8.87a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 1 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 1 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 1 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5zm20 0a0.5 0.5 0 0 1 0.5 -0.5h3a0.5 0.5 0 0 1 0 1h-3a0.5 0.5 0 0 1 -0.5 -0.5zm7 0a0.5 0.5 0 0 1 0.5 -0.5h1a0.5 0.5 0 0 1 0 1h-1a0.5 0.5 0 0 1 -0.5 -0.5zm10 0a0.5 0.5 0 0 1 0.5 -0.5h12a0.5 0.5 0 0 1 0 1h-12a0.5 0.5 0 0 1 -0.5 -0.5z"/>
|
||||
<path fill="#FFF" d="M36.206 58.897h25.423s-7.955 -17.777 8.932 -20.077c15.062 -2.05 21.014 13.427 21.014 13.427s1.786 -8.93 10.743 -7.221c8.832 1.684 15.352 15.889 15.352 15.889h22.137"/>
|
||||
<path fill="#EAEAEE" d="M140.243 56.719h-6.545a0.588 0.588 0 0 1 0 -1.177h6.545a0.588 0.588 0 0 1 0 1.177zm-17.133 0h-1.177a0.588 0.588 0 0 1 0 -1.177h1.177a0.588 0.588 0 0 1 0 1.177zm-61.047 -0.713h-1.892a0.588 0.588 0 1 1 0 -1.176h1.023a24.95 24.95 0 0 1 -0.27 -0.756 0.588 0.588 0 0 1 1.115 -0.377 18.81 18.81 0 0 0 0.562 1.48 0.588 0.588 0 0 1 -0.538 0.83zm-11.304 0h-14.118a0.588 0.588 0 1 1 0 -1.176h14.118a0.588 0.588 0 1 1 0 1.176zm66.928 -0.064a0.588 0.588 0 0 1 -0.515 -0.301 44.263 44.263 0 0 0 -1.827 -2.973 0.588 0.588 0 1 1 0.977 -0.655 45.639 45.639 0 0 1 1.878 3.053 0.588 0.588 0 0 1 -0.513 0.876zm-57.304 -6.042a0.588 0.588 0 0 1 -0.582 -0.507 20.768 20.768 0 0 1 -0.134 -1.205 0.588 0.588 0 0 1 1.173 -0.094 19.388 19.388 0 0 0 0.126 1.136 0.588 0.588 0 0 1 -0.583 0.67zm30.646 -2.594a0.587 0.587 0 0 1 -0.518 -0.31 26.496 26.496 0 0 0 -0.57 -1.001 0.588 0.588 0 1 1 1.01 -0.604 27.467 27.467 0 0 1 0.595 1.047 0.588 0.588 0 0 1 -0.517 0.868zm18.677 -1.503a0.586 0.586 0 0 1 -0.383 -0.142 14.722 14.722 0 0 0 -6.68 -3.536 8.578 8.578 0 0 0 -5.628 0.61 0.588 0.588 0 1 1 -0.54 -1.046 9.747 9.747 0 0 1 6.389 -0.72 15.855 15.855 0 0 1 7.225 3.8 0.588 0.588 0 0 1 -0.383 1.034zm-22.019 -3.335a0.587 0.587 0 0 1 -0.443 -0.201 21.943 21.943 0 0 0 -2.45 -2.41 0.588 0.588 0 0 1 0.755 -0.904 23.255 23.255 0 0 1 2.582 2.54 0.588 0.588 0 0 1 -0.444 0.975zm-24.259 -3.43a0.589 0.589 0 0 1 -0.394 -1.025 14.421 14.421 0 0 1 7.882 -3.254 19.345 19.345 0 0 1 5.959 0.079 0.588 0.588 0 1 1 -0.202 1.158 18.263 18.263 0 0 0 -5.598 -0.072 13.284 13.284 0 0 0 -7.252 2.963 0.59 0.59 0 0 1 -0.395 0.151z"/>
|
||||
<path fill="#FFF" d="M140.974 61.234h-104.772a1.176 1.176 0 0 1 0 -2.353h104.772a1.176 1.176 0 0 1 0 2.353zm-120.522 -46.508h14.187s-4.44 -9.92 4.984 -11.204c8.405 -1.144 11.727 7.493 11.727 7.493s0.907 -4.089 5.995 -4.03c4.755 0.055 8.38 7.915 8.567 8.867h12.354"/>
|
||||
<path fill="#EAEAEE" d="M35.055 13.136h-14.167a0.59 0.59 0 1 1 0 -1.18h14.167a0.59 0.59 0 1 1 0 1.18zm43.647 -0.185h-0.545a0.59 0.59 0 0 1 0 -1.18h0.545a0.59 0.59 0 0 1 0 1.18zm-5.267 0h-3.542a0.59 0.59 0 0 1 0 -1.18h3.542a0.59 0.59 0 0 1 0 1.18zm-21.648 -3.526a0.614 0.614 0 0 1 -0.554 -0.384l-0.087 -0.208a0.59 0.59 0 0 1 0.23 -0.739 5.483 5.483 0 0 1 5.186 -4 7.111 7.111 0 0 1 1.33 0.131 10.622 10.622 0 0 1 5.266 3.045 0.59 0.59 0 0 1 -0.822 0.848 9.484 9.484 0 0 0 -4.665 -2.733 5.934 5.934 0 0 0 -1.109 -0.11c-3.403 0 -4.166 3.526 -4.196 3.677a0.592 0.592 0 0 1 -0.58 0.473zm-16.422 -5.27a0.59 0.59 0 0 1 -0.45 -0.973 6.799 6.799 0 0 1 3.235 -2.027 0.59 0.59 0 0 1 0.352 1.127 5.632 5.632 0 0 0 -2.688 1.665 0.589 0.589 0 0 1 -0.45 0.208zm8.783 -1.988a0.601 0.601 0 0 1 -0.155 -0.02 9.053 9.053 0 0 0 -1.083 -0.224 0.59 0.59 0 0 1 -0.5 -0.669 0.6 0.6 0 0 1 0.668 -0.5 10.36 10.36 0 0 1 1.224 0.253 0.59 0.59 0 0 1 -0.154 1.16z"/>
|
||||
<path fill="#FFF" d="M79.18 17.096h-58.468a1.18 1.18 0 1 1 0 -2.361h58.468a1.18 1.18 0 1 1 0 2.36z"/>
|
||||
<path fill="url(#a)" d="M125.948 119.063h-93.75a2.821 2.821 0 0 1 -2.442 -4.232l46.874 -81.19a2.82 2.82 0 0 1 4.887 0.001l46.874 81.19 -0.001 -0.001a2.821 2.821 0 0 1 -2.442 4.232zm-46.875 -84.333a0.32 0.32 0 0 0 -0.277 0.16l-46.875 81.192a0.32 0.32 0 0 0 0.276 0.481h93.751a0.32 0.32 0 0 0 0.278 -0.48l-0.001 -0.001 -46.874 -81.19a0.32 0.32 0 0 0 -0.278 -0.162z"/>
|
||||
<path fill="#F9F9FA" d="M79.073 34.73a0.32 0.32 0 0 0 -0.277 0.16l-46.875 81.192a0.32 0.32 0 0 0 0.276 0.481h93.751a0.32 0.32 0 0 0 0.278 -0.48l-0.001 -0.001 -46.874 -81.19a0.32 0.32 0 0 0 -0.278 -0.162z"/>
|
||||
<ellipse cx="79.424" cy="140.995" fill="#EAEAEE" rx="49.833" ry="5.802"/>
|
||||
<path fill="url(#b)" d="M79.073 42.313a0.275 0.275 0 0 0 -0.238 0.138l-40.24 69.699a0.275 0.275 0 0 0 0.237 0.413h80.481a0.275 0.275 0 0 0 0.238 -0.412v-0.001l-40.24 -69.699a0.274 0.274 0 0 0 -0.238 -0.138z"/>
|
||||
<path fill="url(#c)" d="M83.67 87.928h-9.19l-1.535 -25.095h12.255l-1.53 25.095zm1.473 11.018c0 3.35 -2.717 6.067 -6.068 6.067 -3.35 0 -6.067 -2.716 -6.067 -6.067s2.716 -6.068 6.067 -6.068a6.1 6.1 0 0 1 6.068 6.068z"/>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 6.3 KiB |
@ -1,35 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
html {
|
||||
color: -moz-DialogText;
|
||||
font: message-box;
|
||||
/* Make sure the background ends to the bottom if there is unused space */
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
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%;
|
||||
}
|
||||
|
||||
#complete-status {
|
||||
column-count: 2;
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- 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/. -->
|
||||
<html>
|
||||
<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>
|
||||
<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>
|
||||
<button id="toggleDirectionality">Toggle :dir</button>
|
||||
<button id="toggleBranding">Toggle branding</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 id="complete-status">
|
||||
<legend>Complete Status</legend>
|
||||
<label class="block"><input type="radio" name="setCompleteStatus" value="">(default)</label>
|
||||
<label class="block"><input type="radio" name="setCompleteStatus" value="processing">Processing</label>
|
||||
<label class="block"><input type="radio" name="setCompleteStatus" value="fail">Fail</label>
|
||||
<label class="block"><input type="radio" name="setCompleteStatus" value="unknown">Unknown</label>
|
||||
<label class="block"><input type="radio" name="setCompleteStatus" value="timeout">Timeout</label>
|
||||
</fieldset>
|
||||
<label class="block"><input type="checkbox" id="setChangesPrevented">Prevent changes</label>
|
||||
|
||||
|
||||
<section class="group">
|
||||
<fieldset>
|
||||
<legend>User Data Errors</legend>
|
||||
<button id="saveVisibleForm" title="Bypasses field validation">Save Visible Form</button>
|
||||
<button id="setBasicCardErrors">Basic Card Errors</button>
|
||||
<button id="setPayerErrors">Payer Errors</button>
|
||||
<button id="setShippingError">Shipping Error</button>
|
||||
<button id="setShippingAddressErrors">Shipping Address Errors</button>
|
||||
|
||||
</fieldset>
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,664 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
const paymentDialog = window.parent.document.querySelector("payment-dialog");
|
||||
// The requestStore should be manipulated for most changes but autofill storage changes
|
||||
// happen through setStateFromParent which includes some consistency checks.
|
||||
const requestStore = paymentDialog.requestStore;
|
||||
|
||||
// keep the payment options checkboxes in sync w. actual state
|
||||
const paymentOptionsUpdater = {
|
||||
stateChangeCallback(state) {
|
||||
this.render(state);
|
||||
},
|
||||
render(state) {
|
||||
let { completeStatus, paymentOptions } = state.request;
|
||||
|
||||
document.getElementById("setChangesPrevented").checked =
|
||||
state.changesPrevented;
|
||||
|
||||
let paymentOptionInputs = document.querySelectorAll(
|
||||
"#paymentOptions input[type='checkbox']"
|
||||
);
|
||||
for (let input of paymentOptionInputs) {
|
||||
if (paymentOptions.hasOwnProperty(input.name)) {
|
||||
input.checked = paymentOptions[input.name];
|
||||
}
|
||||
}
|
||||
|
||||
let completeStatusInputs = document.querySelectorAll(
|
||||
"input[type='radio'][name='setCompleteStatus']"
|
||||
);
|
||||
for (let input of completeStatusInputs) {
|
||||
input.checked = input.value == completeStatus;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let REQUEST_1 = {
|
||||
tabId: 9,
|
||||
topLevelPrincipal: { URI: { displayHost: "debugging.example.com" } },
|
||||
requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
|
||||
completeStatus: "",
|
||||
paymentMethods: [],
|
||||
paymentDetails: {
|
||||
id: "",
|
||||
totalItem: {
|
||||
label: "Demo total",
|
||||
amount: { currency: "EUR", value: "1.00" },
|
||||
pending: false,
|
||||
},
|
||||
displayItems: [
|
||||
{
|
||||
label: "Square",
|
||||
amount: {
|
||||
currency: "USD",
|
||||
value: "5",
|
||||
},
|
||||
},
|
||||
],
|
||||
payerErrors: {},
|
||||
paymentMethodErrors: {},
|
||||
shippingAddressErrors: {},
|
||||
shippingOptions: [
|
||||
{
|
||||
id: "std",
|
||||
label: "Standard (3-5 business days)",
|
||||
amount: {
|
||||
currency: "USD",
|
||||
value: 10,
|
||||
},
|
||||
selected: false,
|
||||
},
|
||||
{
|
||||
id: "super-slow",
|
||||
// Long to test truncation
|
||||
label: "Ssssssssuuuuuuuuupppppeeeeeeerrrrr sssssllllllloooooowwwwww",
|
||||
amount: {
|
||||
currency: "USD",
|
||||
value: 1.5,
|
||||
},
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
modifiers: null,
|
||||
error: "",
|
||||
},
|
||||
paymentOptions: {
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: false,
|
||||
requestPayerPhone: false,
|
||||
requestShipping: true,
|
||||
shippingType: "shipping",
|
||||
},
|
||||
shippingOption: "std",
|
||||
};
|
||||
|
||||
let REQUEST_2 = {
|
||||
tabId: 9,
|
||||
topLevelPrincipal: { URI: { displayHost: "example.com" } },
|
||||
requestId: "3797081f-a96b-c34b-a58b-1083c6e66e25",
|
||||
completeStatus: "",
|
||||
paymentMethods: [
|
||||
{
|
||||
supportedMethods: "basic-card",
|
||||
data: {
|
||||
supportedNetworks: ["amex", "discover", "mastercard", "visa"],
|
||||
},
|
||||
},
|
||||
],
|
||||
paymentDetails: {
|
||||
id: "",
|
||||
totalItem: {
|
||||
label: "",
|
||||
amount: { currency: "CAD", value: "25.75" },
|
||||
pending: false,
|
||||
},
|
||||
displayItems: [
|
||||
{
|
||||
label: "Triangle",
|
||||
amount: {
|
||||
currency: "CAD",
|
||||
value: "3",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Circle",
|
||||
amount: {
|
||||
currency: "EUR",
|
||||
value: "10.50",
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Tax",
|
||||
type: "tax",
|
||||
amount: {
|
||||
currency: "USD",
|
||||
value: "1.50",
|
||||
},
|
||||
},
|
||||
],
|
||||
payerErrors: {},
|
||||
paymentMethoErrors: {},
|
||||
shippingAddressErrors: {},
|
||||
shippingOptions: [
|
||||
{
|
||||
id: "123",
|
||||
label: "Fast (default)",
|
||||
amount: {
|
||||
currency: "USD",
|
||||
value: 10,
|
||||
},
|
||||
selected: true,
|
||||
},
|
||||
{
|
||||
id: "947",
|
||||
label: "Slow",
|
||||
amount: {
|
||||
currency: "USD",
|
||||
value: 1,
|
||||
},
|
||||
selected: false,
|
||||
},
|
||||
],
|
||||
modifiers: [
|
||||
{
|
||||
supportedMethods: "basic-card",
|
||||
total: {
|
||||
label: "Total",
|
||||
amount: {
|
||||
currency: "CAD",
|
||||
value: "28.75",
|
||||
},
|
||||
pending: false,
|
||||
},
|
||||
additionalDisplayItems: [
|
||||
{
|
||||
label: "Credit card fee",
|
||||
amount: {
|
||||
currency: "CAD",
|
||||
value: "1.50",
|
||||
},
|
||||
},
|
||||
],
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
error: "",
|
||||
},
|
||||
paymentOptions: {
|
||||
requestPayerName: false,
|
||||
requestPayerEmail: false,
|
||||
requestPayerPhone: false,
|
||||
requestShipping: true,
|
||||
shippingType: "shipping",
|
||||
},
|
||||
shippingOption: "123",
|
||||
};
|
||||
|
||||
let ADDRESSES_1 = {
|
||||
"48bnds6854t": {
|
||||
"address-level1": "MI",
|
||||
"address-level2": "Some City",
|
||||
country: "US",
|
||||
email: "foo@bar.com",
|
||||
"family-name": "Smith",
|
||||
"given-name": "John",
|
||||
guid: "48bnds6854t",
|
||||
name: "John Smith",
|
||||
"postal-code": "90210",
|
||||
"street-address": "123 Sesame Street,\nApt 40",
|
||||
tel: "+1 519 555-5555",
|
||||
timeLastUsed: 50000,
|
||||
},
|
||||
"68gjdh354j": {
|
||||
"additional-name": "Z.",
|
||||
"address-level1": "CA",
|
||||
"address-level2": "Mountain View",
|
||||
country: "US",
|
||||
"family-name": "Doe",
|
||||
"given-name": "Jane",
|
||||
guid: "68gjdh354j",
|
||||
name: "Jane Z. Doe",
|
||||
"postal-code": "94041",
|
||||
"street-address": "P.O. Box 123",
|
||||
tel: "+1 650 555-5555",
|
||||
timeLastUsed: 30000,
|
||||
},
|
||||
abcde12345: {
|
||||
"address-level2": "Mountain View",
|
||||
country: "US",
|
||||
"family-name": "Fields",
|
||||
"given-name": "Mrs.",
|
||||
guid: "abcde12345",
|
||||
name: "Mrs. Fields",
|
||||
timeLastUsed: 70000,
|
||||
},
|
||||
german1: {
|
||||
"additional-name": "Y.",
|
||||
"address-level1": "",
|
||||
"address-level2": "Berlin",
|
||||
country: "DE",
|
||||
email: "de@example.com",
|
||||
"family-name": "Mouse",
|
||||
"given-name": "Anon",
|
||||
guid: "german1",
|
||||
name: "Anon Y. Mouse",
|
||||
organization: "Mozilla",
|
||||
"postal-code": "10997",
|
||||
"street-address": "Schlesische Str. 27",
|
||||
tel: "+49 30 983333002",
|
||||
timeLastUsed: 10000,
|
||||
},
|
||||
"missing-country": {
|
||||
"address-level1": "ON",
|
||||
"address-level2": "Toronto",
|
||||
"family-name": "Bogard",
|
||||
"given-name": "Kristin",
|
||||
guid: "missing-country",
|
||||
name: "Kristin Bogard",
|
||||
"postal-code": "H0H 0H0",
|
||||
"street-address": "123 Yonge Street\nSuite 2300",
|
||||
tel: "+1 416 555-5555",
|
||||
timeLastUsed: 90000,
|
||||
},
|
||||
TimBR: {
|
||||
"given-name": "Timothy",
|
||||
"additional-name": "João",
|
||||
"family-name": "Berners-Lee",
|
||||
organization: "World Wide Web Consortium",
|
||||
"street-address": "Rua Adalberto Pajuaba, 404",
|
||||
"address-level3": "Campos Elísios",
|
||||
"address-level2": "Ribeirão Preto",
|
||||
"address-level1": "SP",
|
||||
"postal-code": "14055-220",
|
||||
country: "BR",
|
||||
tel: "+0318522222222",
|
||||
email: "timbr@example.org",
|
||||
timeLastUsed: 110000,
|
||||
},
|
||||
};
|
||||
|
||||
let DUPED_ADDRESSES = {
|
||||
a9e830667189: {
|
||||
"street-address": "Unit 1\n1505 Northeast Kentucky Industrial Parkway \n",
|
||||
"address-level2": "Greenup",
|
||||
"address-level1": "KY",
|
||||
"postal-code": "41144",
|
||||
country: "US",
|
||||
email: "bob@example.com",
|
||||
"family-name": "Smith",
|
||||
"given-name": "Bob",
|
||||
guid: "a9e830667189",
|
||||
tel: "+19871234567",
|
||||
name: "Bob Smith",
|
||||
timeLastUsed: 10001,
|
||||
},
|
||||
"72a15aed206d": {
|
||||
"street-address": "1 New St",
|
||||
"address-level2": "York",
|
||||
"address-level1": "SC",
|
||||
"postal-code": "29745",
|
||||
country: "US",
|
||||
"given-name": "Mary Sue",
|
||||
guid: "72a15aed206d",
|
||||
tel: "+19871234567",
|
||||
name: "Mary Sue",
|
||||
"address-line1": "1 New St",
|
||||
timeLastUsed: 10009,
|
||||
},
|
||||
"2b4dce0fbc1f": {
|
||||
"street-address": "123 Park St",
|
||||
"address-level2": "Springfield",
|
||||
"address-level1": "OR",
|
||||
"postal-code": "97403",
|
||||
country: "US",
|
||||
email: "rita@foo.com",
|
||||
"family-name": "Foo",
|
||||
"given-name": "Rita",
|
||||
guid: "2b4dce0fbc1f",
|
||||
name: "Rita Foo",
|
||||
"address-line1": "123 Park St",
|
||||
timeLastUsed: 10005,
|
||||
},
|
||||
"46b2635a5b26": {
|
||||
"street-address": "432 Another St",
|
||||
"address-level2": "Springfield",
|
||||
"address-level1": "OR",
|
||||
"postal-code": "97402",
|
||||
country: "US",
|
||||
email: "rita@foo.com",
|
||||
"family-name": "Foo",
|
||||
"given-name": "Rita",
|
||||
guid: "46b2635a5b26",
|
||||
name: "Rita Foo",
|
||||
"address-line1": "432 Another St",
|
||||
timeLastUsed: 10003,
|
||||
},
|
||||
};
|
||||
|
||||
let BASIC_CARDS_1 = {
|
||||
"53f9d009aed2": {
|
||||
billingAddressGUID: "68gjdh354j",
|
||||
methodName: "basic-card",
|
||||
"cc-number": "************5461",
|
||||
guid: "53f9d009aed2",
|
||||
version: 2,
|
||||
timeCreated: 1505240896213,
|
||||
timeLastModified: 1515609524588,
|
||||
timeLastUsed: 10000,
|
||||
timesUsed: 0,
|
||||
"cc-name": "John Smith",
|
||||
"cc-exp-month": 6,
|
||||
"cc-exp-year": 2024,
|
||||
"cc-type": "visa",
|
||||
"cc-given-name": "John",
|
||||
"cc-additional-name": "",
|
||||
"cc-family-name": "Smith",
|
||||
"cc-exp": "2024-06",
|
||||
},
|
||||
"9h5d4h6f4d1s": {
|
||||
methodName: "basic-card",
|
||||
"cc-number": "************0954",
|
||||
guid: "9h5d4h6f4d1s",
|
||||
version: 2,
|
||||
timeCreated: 1517890536491,
|
||||
timeLastModified: 1517890564518,
|
||||
timeLastUsed: 50000,
|
||||
timesUsed: 0,
|
||||
"cc-name": "Jane Doe",
|
||||
"cc-exp-month": 5,
|
||||
"cc-exp-year": 2023,
|
||||
"cc-type": "mastercard",
|
||||
"cc-given-name": "Jane",
|
||||
"cc-additional-name": "",
|
||||
"cc-family-name": "Doe",
|
||||
"cc-exp": "2023-05",
|
||||
},
|
||||
"123456789abc": {
|
||||
methodName: "basic-card",
|
||||
"cc-number": "************1234",
|
||||
guid: "123456789abc",
|
||||
version: 2,
|
||||
timeCreated: 1517890536491,
|
||||
timeLastModified: 1517890564518,
|
||||
timeLastUsed: 90000,
|
||||
timesUsed: 0,
|
||||
"cc-name": "Jane Fields",
|
||||
"cc-given-name": "Jane",
|
||||
"cc-additional-name": "",
|
||||
"cc-family-name": "Fields",
|
||||
"cc-type": "discover",
|
||||
},
|
||||
"amex-card": {
|
||||
methodName: "basic-card",
|
||||
billingAddressGUID: "68gjdh354j",
|
||||
"cc-number": "************1941",
|
||||
guid: "amex-card",
|
||||
version: 1,
|
||||
timeCreated: 1517890536491,
|
||||
timeLastModified: 1517890564518,
|
||||
timeLastUsed: 70000,
|
||||
timesUsed: 0,
|
||||
"cc-name": "Capt America",
|
||||
"cc-given-name": "Capt",
|
||||
"cc-additional-name": "",
|
||||
"cc-family-name": "America",
|
||||
"cc-type": "amex",
|
||||
"cc-exp-month": 6,
|
||||
"cc-exp-year": 2023,
|
||||
"cc-exp": "2023-06",
|
||||
},
|
||||
"missing-cc-name": {
|
||||
methodName: "basic-card",
|
||||
"cc-number": "************8563",
|
||||
guid: "missing-cc-name",
|
||||
version: 2,
|
||||
timeCreated: 1517890536491,
|
||||
timeLastModified: 1517890564518,
|
||||
timeLastUsed: 30000,
|
||||
timesUsed: 0,
|
||||
"cc-exp-month": 8,
|
||||
"cc-exp-year": 2024,
|
||||
"cc-exp": "2024-08",
|
||||
},
|
||||
};
|
||||
|
||||
let buttonActions = {
|
||||
debugFrame() {
|
||||
let event = new CustomEvent("paymentContentToChrome", {
|
||||
bubbles: true,
|
||||
detail: {
|
||||
messageType: "debugFrame",
|
||||
},
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
},
|
||||
|
||||
delete1Address() {
|
||||
let savedAddresses = Object.assign(
|
||||
{},
|
||||
requestStore.getState().savedAddresses
|
||||
);
|
||||
delete savedAddresses[Object.keys(savedAddresses)[0]];
|
||||
// Use setStateFromParent since it ensures there is no dangling
|
||||
// `selectedShippingAddress` foreign key (FK) reference.
|
||||
paymentDialog.setStateFromParent({
|
||||
savedAddresses,
|
||||
});
|
||||
},
|
||||
|
||||
delete1Card() {
|
||||
let savedBasicCards = Object.assign(
|
||||
{},
|
||||
requestStore.getState().savedBasicCards
|
||||
);
|
||||
delete savedBasicCards[Object.keys(savedBasicCards)[0]];
|
||||
// Use setStateFromParent since it ensures there is no dangling
|
||||
// `selectedPaymentCard` foreign key (FK) reference.
|
||||
paymentDialog.setStateFromParent({
|
||||
savedBasicCards,
|
||||
});
|
||||
},
|
||||
|
||||
logState() {
|
||||
let state = requestStore.getState();
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(state);
|
||||
dump(`${JSON.stringify(state, null, 2)}\n`);
|
||||
},
|
||||
|
||||
refresh() {
|
||||
window.parent.location.reload(true);
|
||||
},
|
||||
|
||||
rerender() {
|
||||
requestStore.setState({});
|
||||
},
|
||||
|
||||
saveVisibleForm() {
|
||||
// Bypasses field validation which is useful to test error handling.
|
||||
paymentDialog
|
||||
.querySelector("#main-container > .page:not([hidden])")
|
||||
.saveRecord();
|
||||
},
|
||||
|
||||
setAddresses1() {
|
||||
paymentDialog.setStateFromParent({ savedAddresses: ADDRESSES_1 });
|
||||
},
|
||||
|
||||
setDupesAddresses() {
|
||||
paymentDialog.setStateFromParent({ savedAddresses: DUPED_ADDRESSES });
|
||||
},
|
||||
|
||||
setBasicCards1() {
|
||||
paymentDialog.setStateFromParent({ savedBasicCards: BASIC_CARDS_1 });
|
||||
},
|
||||
|
||||
setBasicCardErrors() {
|
||||
let request = Object.assign({}, requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign(
|
||||
{},
|
||||
requestStore.getState().request.paymentDetails
|
||||
);
|
||||
request.paymentDetails.paymentMethodErrors = {
|
||||
cardNumber: "",
|
||||
cardholderName: "",
|
||||
cardSecurityCode: "",
|
||||
expiryMonth: "",
|
||||
expiryYear: "",
|
||||
billingAddress: {
|
||||
addressLine:
|
||||
"Can only buy from ROADS, not DRIVES, BOULEVARDS, or STREETS",
|
||||
city: "Can only buy from CITIES, not TOWNSHIPS or VILLAGES",
|
||||
country: "Can only buy from US, not CA",
|
||||
dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
|
||||
organization: "Can only buy from CORPORATIONS, not CONSORTIUMS",
|
||||
phone: "Only allowed to buy from area codes that start with 9",
|
||||
postalCode: "Only allowed to buy from postalCodes that start with 0",
|
||||
recipient: "Can only buy from names that start with J",
|
||||
region: "Can only buy from regions that start with M",
|
||||
regionCode: "Regions must be 1 to 3 characters in length",
|
||||
},
|
||||
};
|
||||
requestStore.setState({
|
||||
request,
|
||||
});
|
||||
},
|
||||
|
||||
setChangesPrevented(evt) {
|
||||
requestStore.setState({
|
||||
changesPrevented: evt.target.checked,
|
||||
});
|
||||
},
|
||||
|
||||
setCompleteStatus() {
|
||||
let input = document.querySelector("[name='setCompleteStatus']:checked");
|
||||
let completeStatus = input.value;
|
||||
let request = requestStore.getState().request;
|
||||
paymentDialog.setStateFromParent({
|
||||
request: Object.assign({}, request, { completeStatus }),
|
||||
});
|
||||
},
|
||||
|
||||
setPayerErrors() {
|
||||
let request = Object.assign({}, requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign(
|
||||
{},
|
||||
requestStore.getState().request.paymentDetails
|
||||
);
|
||||
request.paymentDetails.payerErrors = {
|
||||
email: "Only @mozilla.com emails are supported",
|
||||
name: "Payer name must start with M",
|
||||
phone: "Payer area codes must start with 1",
|
||||
};
|
||||
requestStore.setState({
|
||||
request,
|
||||
});
|
||||
},
|
||||
|
||||
setPaymentOptions() {
|
||||
let options = {};
|
||||
let checkboxes = document.querySelectorAll(
|
||||
"#paymentOptions input[type='checkbox']"
|
||||
);
|
||||
for (let input of checkboxes) {
|
||||
options[input.name] = input.checked;
|
||||
}
|
||||
let req = Object.assign({}, requestStore.getState().request, {
|
||||
paymentOptions: options,
|
||||
});
|
||||
requestStore.setState({ request: req });
|
||||
},
|
||||
|
||||
setRequest1() {
|
||||
paymentDialog.setStateFromParent({ request: REQUEST_1 });
|
||||
},
|
||||
|
||||
setRequest2() {
|
||||
paymentDialog.setStateFromParent({ request: REQUEST_2 });
|
||||
},
|
||||
|
||||
setRequestPayerName() {
|
||||
buttonActions.setPaymentOptions();
|
||||
},
|
||||
setRequestPayerEmail() {
|
||||
buttonActions.setPaymentOptions();
|
||||
},
|
||||
setRequestPayerPhone() {
|
||||
buttonActions.setPaymentOptions();
|
||||
},
|
||||
setRequestShipping() {
|
||||
buttonActions.setPaymentOptions();
|
||||
},
|
||||
|
||||
setShippingError() {
|
||||
let request = Object.assign({}, requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign(
|
||||
{},
|
||||
requestStore.getState().request.paymentDetails
|
||||
);
|
||||
request.paymentDetails.error = "Shipping Error!";
|
||||
request.paymentDetails.shippingOptions = [];
|
||||
requestStore.setState({
|
||||
request,
|
||||
});
|
||||
},
|
||||
|
||||
setShippingAddressErrors() {
|
||||
let request = Object.assign({}, requestStore.getState().request);
|
||||
request.paymentDetails = Object.assign(
|
||||
{},
|
||||
requestStore.getState().request.paymentDetails
|
||||
);
|
||||
request.paymentDetails.shippingAddressErrors = {
|
||||
addressLine: "Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
|
||||
city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
|
||||
country: "Can only ship to USA, not CA",
|
||||
dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
|
||||
organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
|
||||
phone: "Only allowed to ship to area codes that start with 9",
|
||||
postalCode: "Only allowed to ship to postalCodes that start with 0",
|
||||
recipient: "Can only ship to names that start with J",
|
||||
region: "Can only ship to regions that start with M",
|
||||
regionCode: "Regions must be 1 to 3 characters in length",
|
||||
};
|
||||
requestStore.setState({
|
||||
request,
|
||||
});
|
||||
},
|
||||
|
||||
toggleDirectionality() {
|
||||
let body = paymentDialog.ownerDocument.body;
|
||||
body.dir = body.dir == "rtl" ? "ltr" : "rtl";
|
||||
},
|
||||
|
||||
toggleBranding() {
|
||||
for (let container of paymentDialog.querySelectorAll("accepted-cards")) {
|
||||
container.classList.toggle("branded");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
window.addEventListener("click", function onButtonClick(evt) {
|
||||
let id = evt.target.id || evt.target.name;
|
||||
if (!id || typeof buttonActions[id] != "function") {
|
||||
return;
|
||||
}
|
||||
|
||||
buttonActions[id](evt);
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function onDCL() {
|
||||
if (window.location.protocol == "resource:") {
|
||||
// Only show the debug frame button if we're running from a resource URI
|
||||
// so it doesn't show during development over file: or http: since it won't work.
|
||||
// Note that the button still won't work if resource://payments/paymentRequest.xhtml
|
||||
// is manually loaded in a tab but will be shown.
|
||||
document.getElementById("debugFrame").hidden = false;
|
||||
}
|
||||
|
||||
requestStore.subscribe(paymentOptionsUpdater);
|
||||
paymentOptionsUpdater.render(requestStore.getState());
|
||||
});
|
@ -1,28 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* A mixin to forward events to on* methods if defined.
|
||||
*
|
||||
* @param {string} superclass The class to extend.
|
||||
* @returns {class}
|
||||
*/
|
||||
export default function HandleEventMixin(superclass) {
|
||||
return class HandleEvent extends superclass {
|
||||
handleEvent(evt) {
|
||||
function capitalize(str) {
|
||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||
}
|
||||
if (super.handleEvent) {
|
||||
super.handleEvent(evt);
|
||||
}
|
||||
// Check whether event name is a defined function in object.
|
||||
let fn = "on" + capitalize(evt.type);
|
||||
if (this[fn] && typeof this[fn] === "function") {
|
||||
return this[fn](evt);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Define getters and setters for observedAttributes converted to camelCase and
|
||||
* trigger a batched aynchronous call to `render` upon observed
|
||||
* attribute/property changes.
|
||||
*/
|
||||
|
||||
export default function ObservedPropertiesMixin(superClass) {
|
||||
return class ObservedProperties extends superClass {
|
||||
static kebabToCamelCase(name) {
|
||||
return name.replace(/-([a-z])/g, ($0, $1) => $1.toUpperCase());
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this._observedPropertiesMixin = {
|
||||
pendingRender: false,
|
||||
};
|
||||
|
||||
// Reflect property changes for `observedAttributes` to attributes.
|
||||
for (let name of this.constructor.observedAttributes || []) {
|
||||
if (name in this) {
|
||||
// Don't overwrite existing properties.
|
||||
continue;
|
||||
}
|
||||
// Convert attribute names from kebab-case to camelCase properties
|
||||
Object.defineProperty(this, ObservedProperties.kebabToCamelCase(name), {
|
||||
configurable: true,
|
||||
get() {
|
||||
return this.getAttribute(name);
|
||||
},
|
||||
set(value) {
|
||||
if (value === null || value === undefined || value === false) {
|
||||
this.removeAttribute(name);
|
||||
} else {
|
||||
this.setAttribute(name, value);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async _invalidateFromObservedPropertiesMixin() {
|
||||
if (this._observedPropertiesMixin.pendingRender) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._observedPropertiesMixin.pendingRender = true;
|
||||
await Promise.resolve();
|
||||
try {
|
||||
this.render();
|
||||
} finally {
|
||||
this._observedPropertiesMixin.pendingRender = false;
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback(attr, oldValue, newValue) {
|
||||
if (super.attributeChangedCallback) {
|
||||
super.attributeChangedCallback(attr, oldValue, newValue);
|
||||
}
|
||||
if (oldValue === newValue) {
|
||||
return;
|
||||
}
|
||||
this._invalidateFromObservedPropertiesMixin();
|
||||
}
|
||||
};
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
/* 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 PaymentsStore from "../PaymentsStore.js";
|
||||
|
||||
/**
|
||||
* A mixin for a custom element to observe store changes to information about a payment request.
|
||||
*/
|
||||
|
||||
/**
|
||||
* State of the payment request dialog.
|
||||
*/
|
||||
export let requestStore = new PaymentsStore({
|
||||
changesPrevented: false,
|
||||
orderDetailsShowing: false,
|
||||
"basic-card-page": {
|
||||
guid: null,
|
||||
// preserveFieldValues: true,
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
guid: null,
|
||||
},
|
||||
"payer-address-page": {
|
||||
guid: null,
|
||||
},
|
||||
"billing-address-page": {
|
||||
guid: null,
|
||||
},
|
||||
"payment-summary": {},
|
||||
page: {
|
||||
id: "payment-summary",
|
||||
previousId: null,
|
||||
// onboardingWizard: true,
|
||||
// error: "",
|
||||
},
|
||||
request: {
|
||||
completeStatus: "",
|
||||
tabId: null,
|
||||
topLevelPrincipal: { URI: { displayHost: null } },
|
||||
requestId: null,
|
||||
paymentMethods: [],
|
||||
paymentDetails: {
|
||||
id: null,
|
||||
totalItem: { label: null, amount: { currency: null, value: 0 } },
|
||||
displayItems: [],
|
||||
payerErrors: {},
|
||||
paymentMethodErrors: null,
|
||||
shippingAddressErrors: {},
|
||||
shippingOptions: [],
|
||||
modifiers: null,
|
||||
error: "",
|
||||
},
|
||||
paymentOptions: {
|
||||
requestPayerName: false,
|
||||
requestPayerEmail: false,
|
||||
requestPayerPhone: false,
|
||||
requestShipping: false,
|
||||
shippingType: "shipping",
|
||||
},
|
||||
shippingOption: null,
|
||||
},
|
||||
selectedPayerAddress: null,
|
||||
selectedPaymentCard: null,
|
||||
selectedPaymentCardSecurityCode: null,
|
||||
selectedShippingAddress: null,
|
||||
selectedShippingOption: null,
|
||||
savedAddresses: {},
|
||||
savedBasicCards: {},
|
||||
tempAddresses: {},
|
||||
tempBasicCards: {},
|
||||
});
|
||||
|
||||
/**
|
||||
* A mixin to render UI based upon the requestStore and get updated when that store changes.
|
||||
*
|
||||
* Attaches `requestStore` to the element to give access to the store.
|
||||
* @param {class} superClass The class to extend
|
||||
* @returns {class}
|
||||
*/
|
||||
export default function PaymentStateSubscriberMixin(superClass) {
|
||||
return class PaymentStateSubscriber extends superClass {
|
||||
constructor() {
|
||||
super();
|
||||
this.requestStore = requestStore;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.requestStore.subscribe(this);
|
||||
this.render(this.requestStore.getState());
|
||||
if (super.connectedCallback) {
|
||||
super.connectedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.requestStore.unsubscribe(this);
|
||||
if (super.disconnectedCallback) {
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the store upon state changes.
|
||||
* @param {object} state The current state
|
||||
*/
|
||||
stateChangeCallback(state) {
|
||||
this.render(state);
|
||||
}
|
||||
};
|
||||
}
|
@ -1,265 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
:root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
/* Override font-size from in-content/common.css which is too large */
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#debugging-console {
|
||||
/* include the default borders in the max-height */
|
||||
box-sizing: border-box;
|
||||
float: right;
|
||||
height: 100vh;
|
||||
/* Float above the other overlays */
|
||||
position: relative;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
payment-dialog {
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template: "header" auto
|
||||
"main" 1fr
|
||||
"disabled-overlay" auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
payment-dialog > header,
|
||||
.page > .page-body,
|
||||
.page > footer {
|
||||
padding: 0 10%;
|
||||
}
|
||||
|
||||
payment-dialog > header {
|
||||
border-bottom: 1px solid rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
/* Wrap so that the error text appears full-width above the rest of the contents */
|
||||
flex-wrap: wrap;
|
||||
/* from visual spec: */
|
||||
padding-bottom: 19px;
|
||||
padding-top: 19px;
|
||||
}
|
||||
|
||||
payment-dialog > header > .page-error:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
payment-dialog > header > .page-error {
|
||||
background: #D70022;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
padding: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
display: flex;
|
||||
grid-area: main;
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page > .page-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
/* The area above the footer should scroll, if necessary. */
|
||||
overflow: auto;
|
||||
padding-top: 18px;
|
||||
}
|
||||
|
||||
.page > .page-body > h2:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-error {
|
||||
color: #D70022;
|
||||
}
|
||||
|
||||
.manage-text {
|
||||
margin: 0;
|
||||
padding: 18px 0;
|
||||
}
|
||||
|
||||
.page > footer {
|
||||
align-items: center;
|
||||
justify-content: end;
|
||||
background-color: #eaeaee;
|
||||
display: flex;
|
||||
/* from visual spec: */
|
||||
padding-top: 20px;
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
#order-details-overlay {
|
||||
background-color: var(--in-content-page-background);
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#total {
|
||||
flex: 1 1 auto;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#total > currency-amount {
|
||||
color: var(--in-content-link-color);
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
#total > currency-amount > .currency-code {
|
||||
color: GrayText;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
#total > div {
|
||||
color: GrayText;
|
||||
}
|
||||
|
||||
#view-all {
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
payment-dialog[complete-status="processing"] #pay {
|
||||
/* Force opacity to 1 even when disabled in the processing state. */
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
payment-dialog #pay::before {
|
||||
-moz-context-properties: fill;
|
||||
content: url(chrome://browser/skin/connection-secure.svg);
|
||||
fill: currentColor;
|
||||
height: 16px;
|
||||
margin-inline-end: 0.5em;
|
||||
vertical-align: text-bottom;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
payment-dialog[changes-prevented][complete-status="fail"] #pay,
|
||||
payment-dialog[changes-prevented][complete-status="unknown"] #pay,
|
||||
payment-dialog[changes-prevented][complete-status="processing"] #pay,
|
||||
payment-dialog[changes-prevented][complete-status="success"] #pay {
|
||||
/* Show the pay button above #disabled-overlay */
|
||||
position: relative;
|
||||
z-index: 51;
|
||||
}
|
||||
|
||||
#disabled-overlay {
|
||||
background: white;
|
||||
grid-area: disabled-overlay;
|
||||
opacity: 0.6;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
/* z-index must be greater than some positioned fields and #pay with z-index
|
||||
but less than 99, the z-index of the debugging console. */
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.persist-checkbox {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.persist-checkbox > label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-tooltip {
|
||||
display: inline-block;
|
||||
background-image: url(chrome://global/skin/icons/help.svg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 2px 4px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.info-tooltip:focus::after,
|
||||
.info-tooltip:hover::after {
|
||||
content: attr(aria-label);
|
||||
display: block;
|
||||
position: absolute;
|
||||
padding: 3px 5px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #bebebf;
|
||||
box-shadow: 1px 1px 3px #bebebf;
|
||||
font-size: smaller;
|
||||
line-height: normal;
|
||||
width: 188px;
|
||||
/* Center the tooltip over the (i) icon (188px / 2 - 5px (padding) - 1px (border)). */
|
||||
left: -86px;
|
||||
bottom: 20px;
|
||||
}
|
||||
|
||||
.info-tooltip:dir(rtl):focus::after,
|
||||
.info-tooltip:dir(rtl):hover::after {
|
||||
left: auto;
|
||||
right: -86px;
|
||||
}
|
||||
|
||||
.csc.info-tooltip:focus::after,
|
||||
.csc.info-tooltip:hover::after {
|
||||
/* Right-align the tooltip over the (i) icon (-188px - 60px (padding) - 2px (border) + 4px ((i) start padding) + 16px ((i) icon width)). */
|
||||
left: -226px;
|
||||
background-position: top 5px left 5px;
|
||||
background-image: url(./containers/cvv-hint-image-back.svg);
|
||||
background-repeat: no-repeat;
|
||||
padding-inline-start: 55px;
|
||||
}
|
||||
|
||||
.csc.info-tooltip[cc-type="amex"]::after {
|
||||
background-image: url(./containers/cvv-hint-image-front.svg);
|
||||
}
|
||||
|
||||
.csc.info-tooltip:dir(rtl):focus::after,
|
||||
.csc.info-tooltip:dir(rtl):hover::after {
|
||||
left: auto;
|
||||
/* Left-align the tooltip over the (i) icon (-188px - 60px (padding) - 2px (border) + 4px ((i) start padding) + 16px ((i) icon width)). */
|
||||
right: -226px;
|
||||
background-position: top 5px right 5px;
|
||||
}
|
||||
|
||||
.branding {
|
||||
background-image: url(chrome://branding/content/icon32.png);
|
||||
background-size: 16px;
|
||||
background-repeat: no-repeat;
|
||||
background-position: left center;
|
||||
padding-inline-start: 20px;
|
||||
line-height: 20px;
|
||||
margin-inline-end: auto;
|
||||
}
|
||||
|
||||
.branding:dir(rtl) {
|
||||
background-position: right center;
|
||||
}
|
@ -1,356 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Loaded in the unprivileged frame of each payment dialog.
|
||||
*
|
||||
* Communicates with privileged code via DOM Events.
|
||||
*/
|
||||
|
||||
/* import-globals-from unprivileged-fallbacks.js */
|
||||
|
||||
var paymentRequest = {
|
||||
_nextMessageID: 1,
|
||||
domReadyPromise: null,
|
||||
|
||||
init() {
|
||||
// listen to content
|
||||
window.addEventListener("paymentChromeToContent", this);
|
||||
|
||||
window.addEventListener("keydown", this);
|
||||
|
||||
this.domReadyPromise = new Promise(function dcl(resolve) {
|
||||
window.addEventListener("DOMContentLoaded", resolve, { once: true });
|
||||
}).then(this.handleEvent.bind(this));
|
||||
|
||||
// This scope is now ready to listen to the initialization data
|
||||
this.sendMessageToChrome("initializeRequest");
|
||||
},
|
||||
|
||||
handleEvent(event) {
|
||||
switch (event.type) {
|
||||
case "DOMContentLoaded": {
|
||||
this.onPaymentRequestLoad();
|
||||
break;
|
||||
}
|
||||
case "keydown": {
|
||||
if (event.code != "KeyD" || !event.altKey || !event.ctrlKey) {
|
||||
break;
|
||||
}
|
||||
this.toggleDebuggingConsole();
|
||||
break;
|
||||
}
|
||||
case "unload": {
|
||||
this.onPaymentRequestUnload();
|
||||
break;
|
||||
}
|
||||
case "paymentChromeToContent": {
|
||||
this.onChromeToContent(event);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Unexpected event type");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {string} messageType
|
||||
* @param {[object]} detail
|
||||
* @returns {number} message ID to be able to identify a reply (where applicable).
|
||||
*/
|
||||
sendMessageToChrome(messageType, detail = {}) {
|
||||
let messageID = this._nextMessageID++;
|
||||
log.debug("sendMessageToChrome:", messageType, messageID, detail);
|
||||
let event = new CustomEvent("paymentContentToChrome", {
|
||||
bubbles: true,
|
||||
detail: Object.assign(
|
||||
{
|
||||
messageType,
|
||||
messageID,
|
||||
},
|
||||
detail
|
||||
),
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
return messageID;
|
||||
},
|
||||
|
||||
toggleDebuggingConsole() {
|
||||
let debuggingConsole = document.getElementById("debugging-console");
|
||||
if (debuggingConsole.hidden && !debuggingConsole.src) {
|
||||
debuggingConsole.src = "debugging.html";
|
||||
}
|
||||
debuggingConsole.hidden = !debuggingConsole.hidden;
|
||||
},
|
||||
|
||||
onChromeToContent({ detail }) {
|
||||
let { messageType } = detail;
|
||||
log.debug("onChromeToContent:", messageType);
|
||||
|
||||
switch (messageType) {
|
||||
case "responseSent": {
|
||||
let { request } = document
|
||||
.querySelector("payment-dialog")
|
||||
.requestStore.getState();
|
||||
document.querySelector("payment-dialog").requestStore.setState({
|
||||
changesPrevented: true,
|
||||
request: Object.assign({}, request, { completeStatus: "processing" }),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "showPaymentRequest": {
|
||||
this.onShowPaymentRequest(detail);
|
||||
break;
|
||||
}
|
||||
case "updateState": {
|
||||
document.querySelector("payment-dialog").setStateFromParent(detail);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onPaymentRequestLoad() {
|
||||
log.debug("onPaymentRequestLoad");
|
||||
window.addEventListener("unload", this, { once: true });
|
||||
|
||||
// Automatically show the debugging console if loaded with a truthy `debug` query parameter.
|
||||
if (new URLSearchParams(location.search).get("debug")) {
|
||||
this.toggleDebuggingConsole();
|
||||
}
|
||||
},
|
||||
|
||||
async onShowPaymentRequest(detail) {
|
||||
// Handle getting called before the DOM is ready.
|
||||
log.debug("onShowPaymentRequest:", detail);
|
||||
await this.domReadyPromise;
|
||||
|
||||
log.debug("onShowPaymentRequest: domReadyPromise resolved");
|
||||
log.debug("onShowPaymentRequest, isPrivate?", detail.isPrivate);
|
||||
|
||||
let paymentDialog = document.querySelector("payment-dialog");
|
||||
let state = {
|
||||
request: detail.request,
|
||||
savedAddresses: detail.savedAddresses,
|
||||
savedBasicCards: detail.savedBasicCards,
|
||||
// Temp records can exist upon a reload during development.
|
||||
tempAddresses: detail.tempAddresses,
|
||||
tempBasicCards: detail.tempBasicCards,
|
||||
isPrivate: detail.isPrivate,
|
||||
page: {
|
||||
id: "payment-summary",
|
||||
},
|
||||
};
|
||||
|
||||
let hasSavedAddresses = Object.keys(this.getAddresses(state)).length != 0;
|
||||
let hasSavedCards = Object.keys(this.getBasicCards(state)).length != 0;
|
||||
let shippingRequested = state.request.paymentOptions.requestShipping;
|
||||
|
||||
// Onboarding wizard flow.
|
||||
if (!hasSavedAddresses && shippingRequested) {
|
||||
state.page = {
|
||||
id: "shipping-address-page",
|
||||
onboardingWizard: true,
|
||||
};
|
||||
|
||||
state["shipping-address-page"] = {
|
||||
guid: null,
|
||||
};
|
||||
} else if (!hasSavedAddresses && !hasSavedCards) {
|
||||
state.page = {
|
||||
id: "billing-address-page",
|
||||
onboardingWizard: true,
|
||||
};
|
||||
|
||||
state["billing-address-page"] = {
|
||||
guid: null,
|
||||
};
|
||||
} else if (!hasSavedCards) {
|
||||
state.page = {
|
||||
id: "basic-card-page",
|
||||
onboardingWizard: true,
|
||||
};
|
||||
state["basic-card-page"] = {
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
};
|
||||
}
|
||||
|
||||
await paymentDialog.setStateFromParent(state);
|
||||
|
||||
this.sendMessageToChrome("paymentDialogReady");
|
||||
},
|
||||
|
||||
openPreferences() {
|
||||
this.sendMessageToChrome("openPreferences");
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.sendMessageToChrome("paymentCancel");
|
||||
},
|
||||
|
||||
pay(data) {
|
||||
this.sendMessageToChrome("pay", data);
|
||||
},
|
||||
|
||||
closeDialog() {
|
||||
this.sendMessageToChrome("closeDialog");
|
||||
},
|
||||
|
||||
changePaymentMethod(data) {
|
||||
this.sendMessageToChrome("changePaymentMethod", data);
|
||||
},
|
||||
|
||||
changeShippingAddress(data) {
|
||||
this.sendMessageToChrome("changeShippingAddress", data);
|
||||
},
|
||||
|
||||
changeShippingOption(data) {
|
||||
this.sendMessageToChrome("changeShippingOption", data);
|
||||
},
|
||||
|
||||
changePayerAddress(data) {
|
||||
this.sendMessageToChrome("changePayerAddress", data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add/update an autofill storage record.
|
||||
*
|
||||
* If the the `guid` argument is provided update the record; otherwise, add it.
|
||||
* @param {string} collectionName The autofill collection that record belongs to.
|
||||
* @param {object} record The autofill record to add/update
|
||||
* @param {string} [guid] The guid of the autofill record to update
|
||||
* @returns {Promise} when the update response is received
|
||||
*/
|
||||
updateAutofillRecord(collectionName, record, guid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let messageID = this.sendMessageToChrome("updateAutofillRecord", {
|
||||
collectionName,
|
||||
guid,
|
||||
record,
|
||||
});
|
||||
|
||||
window.addEventListener("paymentChromeToContent", function onMsg({
|
||||
detail,
|
||||
}) {
|
||||
if (
|
||||
detail.messageType != "updateAutofillRecord:Response" ||
|
||||
detail.messageID != messageID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
log.debug("updateAutofillRecord: response:", detail);
|
||||
window.removeEventListener("paymentChromeToContent", onMsg);
|
||||
document
|
||||
.querySelector("payment-dialog")
|
||||
.setStateFromParent(detail.stateChange);
|
||||
if (detail.error) {
|
||||
reject(detail);
|
||||
} else {
|
||||
resolve(detail);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} state object representing the UI state
|
||||
* @param {string} selectedMethodID (GUID) uniquely identifying the selected payment method
|
||||
* @returns {object?} the applicable modifier for the payment method
|
||||
*/
|
||||
getModifierForPaymentMethod(state, selectedMethodID) {
|
||||
let basicCards = this.getBasicCards(state);
|
||||
let selectedMethod = basicCards[selectedMethodID] || null;
|
||||
if (selectedMethod && selectedMethod.methodName !== "basic-card") {
|
||||
throw new Error(
|
||||
`${selectedMethod.methodName} (${selectedMethodID}) ` +
|
||||
`is not a supported payment method`
|
||||
);
|
||||
}
|
||||
let modifiers = state.request.paymentDetails.modifiers;
|
||||
if (!selectedMethod || !modifiers || !modifiers.length) {
|
||||
return null;
|
||||
}
|
||||
let appliedModifier = modifiers.find(modifier => {
|
||||
// take the first matching modifier
|
||||
if (
|
||||
modifier.supportedMethods &&
|
||||
modifier.supportedMethods != selectedMethod.methodName
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
let supportedNetworks =
|
||||
(modifier.data && modifier.data.supportedNetworks) || [];
|
||||
return (
|
||||
supportedNetworks.length == 0 ||
|
||||
supportedNetworks.includes(selectedMethod["cc-type"])
|
||||
);
|
||||
});
|
||||
return appliedModifier || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} state object representing the UI state
|
||||
* @returns {object} in the shape of `nsIPaymentItem` representing the total
|
||||
* that applies to the selected payment method.
|
||||
*/
|
||||
getTotalItem(state) {
|
||||
let methodID = state.selectedPaymentCard;
|
||||
if (methodID) {
|
||||
let modifier = paymentRequest.getModifierForPaymentMethod(
|
||||
state,
|
||||
methodID
|
||||
);
|
||||
if (modifier && modifier.hasOwnProperty("total")) {
|
||||
return modifier.total;
|
||||
}
|
||||
}
|
||||
return state.request.paymentDetails.totalItem;
|
||||
},
|
||||
|
||||
onPaymentRequestUnload() {
|
||||
// remove listeners that may be used multiple times here
|
||||
window.removeEventListener("paymentChromeToContent", this);
|
||||
},
|
||||
|
||||
_sortObjectsByTimeLastUsed(objects) {
|
||||
let sortedValues = Object.values(objects).sort((a, b) => {
|
||||
let aLastUsed = a.timeLastUsed || a.timeLastModified;
|
||||
let bLastUsed = b.timeLastUsed || b.timeLastModified;
|
||||
return bLastUsed - aLastUsed;
|
||||
});
|
||||
let sortedObjects = {};
|
||||
for (let obj of sortedValues) {
|
||||
sortedObjects[obj.guid] = obj;
|
||||
}
|
||||
return sortedObjects;
|
||||
},
|
||||
|
||||
getAddresses(state) {
|
||||
let addresses = Object.assign(
|
||||
{},
|
||||
state.savedAddresses,
|
||||
state.tempAddresses
|
||||
);
|
||||
return this._sortObjectsByTimeLastUsed(addresses);
|
||||
},
|
||||
|
||||
getBasicCards(state) {
|
||||
let cards = Object.assign({}, state.savedBasicCards, state.tempBasicCards);
|
||||
return this._sortObjectsByTimeLastUsed(cards);
|
||||
},
|
||||
|
||||
maybeCreateFieldErrorElement(container) {
|
||||
let span = container.querySelector(".error-text");
|
||||
if (!span) {
|
||||
span = document.createElement("span");
|
||||
span.className = "error-text";
|
||||
container.appendChild(span);
|
||||
}
|
||||
return span;
|
||||
},
|
||||
};
|
||||
|
||||
paymentRequest.init();
|
||||
|
||||
export default paymentRequest;
|
@ -1,303 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- 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/. -->
|
||||
<!DOCTYPE html [
|
||||
<!ENTITY % globalDTD SYSTEM "chrome://global/locale/global.dtd">
|
||||
%globalDTD;
|
||||
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
|
||||
%brandDTD;
|
||||
|
||||
<!ENTITY viewAllItems "View All Items">
|
||||
<!ENTITY paymentSummaryTitle "Your Payment">
|
||||
<!ENTITY header.payTo "Pay to">
|
||||
<!ENTITY fieldRequiredSymbol "*">
|
||||
|
||||
<!ENTITY shippingAddressLabel "Shipping Address">
|
||||
<!ENTITY deliveryAddressLabel "Delivery Address">
|
||||
<!ENTITY pickupAddressLabel "Pickup Address">
|
||||
<!ENTITY shippingOptionsLabel "Shipping Options">
|
||||
<!ENTITY deliveryOptionsLabel "Delivery Options">
|
||||
<!ENTITY pickupOptionsLabel "Pickup Options">
|
||||
<!ENTITY shippingGenericError "Can’t ship to this address. Select a different address.">
|
||||
<!ENTITY deliveryGenericError "Can’t deliver to this address. Select a different address.">
|
||||
<!ENTITY pickupGenericError "Can’t pick up from this address. Select a different address.">
|
||||
<!ENTITY paymentMethodsLabel "Payment Method">
|
||||
<!ENTITY address.fieldSeparator ", ">
|
||||
<!ENTITY address.addLink.label "Add">
|
||||
<!ENTITY address.editLink.label "Edit">
|
||||
<!ENTITY basicCard.addLink.label "Add">
|
||||
<!ENTITY basicCard.editLink.label "Edit">
|
||||
<!ENTITY payer.addLink.label "Add">
|
||||
<!ENTITY payer.editLink.label "Edit">
|
||||
<!ENTITY shippingAddress.addPage.title "Add Shipping Address">
|
||||
<!ENTITY shippingAddress.editPage.title "Edit Shipping Address">
|
||||
<!ENTITY deliveryAddress.addPage.title "Add Delivery Address">
|
||||
<!ENTITY deliveryAddress.editPage.title "Edit Delivery Address">
|
||||
<!ENTITY pickupAddress.addPage.title "Add Pickup Address">
|
||||
<!ENTITY pickupAddress.editPage.title "Edit Pickup Address">
|
||||
<!ENTITY billingAddress.addPage.title "Add Billing Address">
|
||||
<!ENTITY billingAddress.editPage.title "Edit Billing Address">
|
||||
<!ENTITY basicCard.addPage.title "Add Credit Card">
|
||||
<!ENTITY basicCard.editPage.title "Edit Credit Card">
|
||||
<!ENTITY basicCard.csc.placeholder "CVV">
|
||||
<!ENTITY basicCard.csc.back.infoTooltip "3 digit number found on the back of your credit card.">
|
||||
<!ENTITY basicCard.csc.front.infoTooltip "3 digit number found on the front of your credit card.">
|
||||
<!ENTITY payer.addPage.title "Add Payer Contact">
|
||||
<!ENTITY payer.editPage.title "Edit Payer Contact">
|
||||
<!ENTITY payerLabel "Contact Information">
|
||||
<!ENTITY manageInPreferences "Manage saved address and credit card information in <a>&brandShortName; Preferences</a>.">
|
||||
<!ENTITY manageInOptions "Manage saved address and credit card information in <a>&brandShortName; Options</a>.">
|
||||
<!ENTITY cancelPaymentButton.label "Cancel">
|
||||
<!ENTITY approvePaymentButton.label "Pay">
|
||||
<!ENTITY processingPaymentButton.label "Processing">
|
||||
<!ENTITY successPaymentButton.label "Done">
|
||||
<!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.nextButton.label "Next">
|
||||
<!ENTITY basicCardPage.updateButton.label "Update">
|
||||
<!ENTITY basicCardPage.persistCheckbox.label "Save credit card to &brandShortName; (CVV will not be saved)">
|
||||
<!ENTITY basicCardPage.persistCheckbox.infoTooltip "&brandShortName; can securely store your credit card information to use in forms like this, so you don’t have to enter it every time.">
|
||||
<!ENTITY addressPage.error.genericSave "There was an error saving the address.">
|
||||
<!ENTITY addressPage.cancelButton.label "Cancel">
|
||||
<!ENTITY addressPage.backButton.label "Back">
|
||||
<!ENTITY addressPage.nextButton.label "Next">
|
||||
<!ENTITY addressPage.updateButton.label "Update">
|
||||
<!ENTITY addressPage.persistCheckbox.label "Save address to &brandShortName;">
|
||||
<!ENTITY addressPage.persistCheckbox.infoTooltip "&brandShortName; can add your address to forms like this, so you don’t have to type it every time.">
|
||||
<!ENTITY failErrorPage.title "We couldn’t complete your payment to **host-name**">
|
||||
<!ENTITY failErrorPage.suggestionHeading "The most likely cause is a hiccup with your credit card.">
|
||||
<!ENTITY failErrorPage.suggestion1 "Make sure the card you’re using hasn’t expired">
|
||||
<!ENTITY failErrorPage.suggestion2 "Double check the card number and expiration date">
|
||||
<!ENTITY failErrorPage.suggestion3 "If your credit card information is correct, contact the merchant for more information">
|
||||
<!ENTITY failErrorPage.doneButton.label "Close">
|
||||
<!ENTITY timeoutErrorPage.title "**host-name** is taking too long to respond.">
|
||||
<!ENTITY timeoutErrorPage.suggestionHeading "The most likely cause is a temporary connection hiccup. Open a new tab to check your network connection or click “Close” to try again.">
|
||||
<!ENTITY timeoutErrorPage.doneButton.label "Close">
|
||||
<!ENTITY webPaymentsBranding.label "&brandShortName; Checkout">
|
||||
<!ENTITY invalidOption.label "Missing or invalid information">
|
||||
<!ENTITY acceptedCards.label "Merchant accepts:">
|
||||
]>
|
||||
<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:"/>
|
||||
|
||||
<link rel="stylesheet" href="chrome://global/skin/in-content/common.css"/>
|
||||
<link rel="stylesheet" href="resource://formautofill/editDialog-shared.css"/>
|
||||
<link rel="stylesheet" href="resource://formautofill/editAddress.css"/>
|
||||
<link rel="stylesheet" href="resource://formautofill/editCreditCard.css"/>
|
||||
<link rel="stylesheet" href="resource://formautofill/editDialog.css"/>
|
||||
<link rel="stylesheet" href="paymentRequest.css"/>
|
||||
<link rel="stylesheet" href="components/rich-select.css"/>
|
||||
<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="components/accepted-cards.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>
|
||||
|
||||
<template id="payment-dialog-template">
|
||||
<header>
|
||||
<div class="page-error"
|
||||
data-shipping-generic-error="&shippingGenericError;"
|
||||
data-delivery-generic-error="&deliveryGenericError;"
|
||||
data-pickup-generic-error="&pickupGenericError;"
|
||||
aria-live="polite"></div>
|
||||
<div id="total">
|
||||
<currency-amount display-code="display-code"></currency-amount>
|
||||
<div>&header.payTo; <span id="host-name"></span></div>
|
||||
</div>
|
||||
<div id="top-buttons" hidden="hidden">
|
||||
<button id="view-all" class="closed">&viewAllItems;</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="main-container">
|
||||
<payment-request-page id="payment-summary">
|
||||
<div class="page-body">
|
||||
<address-picker class="shipping-related"
|
||||
data-add-link-label="&address.addLink.label;"
|
||||
data-edit-link-label="&address.editLink.label;"
|
||||
data-field-separator="&address.fieldSeparator;"
|
||||
data-shipping-address-label="&shippingAddressLabel;"
|
||||
data-delivery-address-label="&deliveryAddressLabel;"
|
||||
data-pickup-address-label="&pickupAddressLabel;"
|
||||
data-invalid-label="&invalidOption.label;"
|
||||
selected-state-key="selectedShippingAddress"></address-picker>
|
||||
|
||||
<shipping-option-picker class="shipping-related"
|
||||
data-shipping-options-label="&shippingOptionsLabel;"
|
||||
data-delivery-options-label="&deliveryOptionsLabel;"
|
||||
data-pickup-options-label="&pickupOptionsLabel;"></shipping-option-picker>
|
||||
|
||||
<payment-method-picker selected-state-key="selectedPaymentCard"
|
||||
data-add-link-label="&basicCard.addLink.label;"
|
||||
data-edit-link-label="&basicCard.editLink.label;"
|
||||
data-csc-placeholder="&basicCard.csc.placeholder;"
|
||||
data-csc-back-tooltip="&basicCard.csc.back.infoTooltip;"
|
||||
data-csc-front-tooltip="&basicCard.csc.front.infoTooltip;"
|
||||
data-invalid-label="&invalidOption.label;"
|
||||
label="&paymentMethodsLabel;">
|
||||
</payment-method-picker>
|
||||
<accepted-cards hidden="hidden" label="&acceptedCards.label;"></accepted-cards>
|
||||
<address-picker class="payer-related"
|
||||
label="&payerLabel;"
|
||||
data-add-link-label="&payer.addLink.label;"
|
||||
data-edit-link-label="&payer.editLink.label;"
|
||||
data-field-separator="&address.fieldSeparator;"
|
||||
data-invalid-label="&invalidOption.label;"
|
||||
selected-state-key="selectedPayerAddress"></address-picker>
|
||||
|
||||
<p class="manage-text">
|
||||
<span hidden="hidden" data-os="mac">&manageInPreferences;</span>
|
||||
<span hidden="hidden">&manageInOptions;</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<span class="branding">&webPaymentsBranding.label;</span>
|
||||
<button id="cancel">&cancelPaymentButton.label;</button>
|
||||
<button id="pay"
|
||||
class="primary"
|
||||
data-label="&approvePaymentButton.label;"
|
||||
data-processing-label="&processingPaymentButton.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>
|
||||
|
||||
<basic-card-form id="basic-card-page"
|
||||
data-add-basic-card-title="&basicCard.addPage.title;"
|
||||
data-edit-basic-card-title="&basicCard.editPage.title;"
|
||||
data-error-generic-save="&basicCardPage.error.genericSave;"
|
||||
|
||||
data-address-add-link-label="&basicCardPage.addressAddLink.label;"
|
||||
data-address-edit-link-label="&basicCardPage.addressEditLink.label;"
|
||||
|
||||
data-invalid-address-label="&invalidOption.label;"
|
||||
data-address-field-separator="&address.fieldSeparator;"
|
||||
data-back-button-label="&basicCardPage.backButton.label;"
|
||||
data-next-button-label="&basicCardPage.nextButton.label;"
|
||||
data-update-button-label="&basicCardPage.updateButton.label;"
|
||||
data-cancel-button-label="&cancelPaymentButton.label;"
|
||||
data-persist-checkbox-label="&basicCardPage.persistCheckbox.label;"
|
||||
data-persist-checkbox-info-tooltip="&basicCardPage.persistCheckbox.infoTooltip;"
|
||||
data-csc-placeholder="&basicCard.csc.placeholder;"
|
||||
data-csc-back-info-tooltip="&basicCard.csc.back.infoTooltip;"
|
||||
data-csc-front-info-tooltip="&basicCard.csc.front.infoTooltip;"
|
||||
data-accepted-cards-label="&acceptedCards.label;"
|
||||
data-field-required-symbol="&fieldRequiredSymbol;"
|
||||
hidden="hidden"></basic-card-form>
|
||||
|
||||
<address-form id="shipping-address-page"
|
||||
data-title-add="&shippingAddress.addPage.title;"
|
||||
data-title-edit="&shippingAddress.editPage.title;"
|
||||
data-error-generic-save="&addressPage.error.genericSave;"
|
||||
data-cancel-button-label="&addressPage.cancelButton.label;"
|
||||
data-back-button-label="&addressPage.backButton.label;"
|
||||
data-next-button-label="&addressPage.nextButton.label;"
|
||||
data-update-button-label="&addressPage.updateButton.label;"
|
||||
data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
|
||||
data-persist-checkbox-info-tooltip="&addressPage.persistCheckbox.infoTooltip;"
|
||||
data-field-required-symbol="&fieldRequiredSymbol;"
|
||||
hidden="hidden"
|
||||
selected-state-key="selectedShippingAddress"></address-form>
|
||||
|
||||
<address-form id="payer-address-page"
|
||||
data-title-add="&payer.addPage.title;"
|
||||
data-title-edit="&payer.editPage.title;"
|
||||
data-error-generic-save="&addressPage.error.genericSave;"
|
||||
data-cancel-button-label="&addressPage.cancelButton.label;"
|
||||
data-back-button-label="&addressPage.backButton.label;"
|
||||
data-next-button-label="&addressPage.nextButton.label;"
|
||||
data-update-button-label="&addressPage.updateButton.label;"
|
||||
data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
|
||||
data-persist-checkbox-info-tooltip="&addressPage.persistCheckbox.infoTooltip;"
|
||||
data-field-required-symbol="&fieldRequiredSymbol;"
|
||||
hidden="hidden"
|
||||
selected-state-key="selectedPayerAddress"></address-form>
|
||||
|
||||
<address-form id="billing-address-page"
|
||||
data-title-add="&billingAddress.addPage.title;"
|
||||
data-title-edit="&billingAddress.editPage.title;"
|
||||
data-error-generic-save="&addressPage.error.genericSave;"
|
||||
data-cancel-button-label="&addressPage.cancelButton.label;"
|
||||
data-back-button-label="&addressPage.backButton.label;"
|
||||
data-next-button-label="&addressPage.nextButton.label;"
|
||||
data-update-button-label="&addressPage.updateButton.label;"
|
||||
data-persist-checkbox-label="&addressPage.persistCheckbox.label;"
|
||||
data-persist-checkbox-info-tooltip="&addressPage.persistCheckbox.infoTooltip;"
|
||||
data-field-required-symbol="&fieldRequiredSymbol;"
|
||||
hidden="hidden"
|
||||
selected-state-key="basic-card-page|billingAddressGUID"></address-form>
|
||||
|
||||
<completion-error-page id="completion-timeout-error" class="illustrated"
|
||||
data-page-title="&timeoutErrorPage.title;"
|
||||
data-suggestion-heading="&timeoutErrorPage.suggestionHeading;"
|
||||
data-branding-label="&webPaymentsBranding.label;"
|
||||
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-suggestion-heading="&failErrorPage.suggestionHeading;"
|
||||
data-suggestion-1="&failErrorPage.suggestion1;"
|
||||
data-suggestion-2="&failErrorPage.suggestion2;"
|
||||
data-suggestion-3="&failErrorPage.suggestion3;"
|
||||
data-branding-label="&webPaymentsBranding.label;"
|
||||
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">
|
||||
<ul class="main-list"></ul>
|
||||
<ul class="footer-items-list"></ul>
|
||||
|
||||
<div class="details-total">
|
||||
<h2 class="label">&orderTotalLabel;</h2>
|
||||
<currency-amount></currency-amount>
|
||||
</div>
|
||||
</template>
|
||||
</head>
|
||||
<body dir="&locale.dir;">
|
||||
<iframe id="debugging-console"
|
||||
hidden="hidden">
|
||||
</iframe>
|
||||
<payment-dialog data-shipping-address-title-add="&shippingAddress.addPage.title;"
|
||||
data-shipping-address-title-edit="&shippingAddress.editPage.title;"
|
||||
data-delivery-address-title-add="&deliveryAddress.addPage.title;"
|
||||
data-delivery-address-title-edit="&deliveryAddress.editPage.title;"
|
||||
data-pickup-address-title-add="&pickupAddress.addPage.title;"
|
||||
data-pickup-address-title-edit="&pickupAddress.editPage.title;"
|
||||
data-billing-address-title-add="&billingAddress.addPage.title;"
|
||||
data-payer-title-add="&payer.addPage.title;"
|
||||
data-payer-title-edit="&payer.editPage.title;"></payment-dialog>
|
||||
</body>
|
||||
</html>
|
@ -1,159 +0,0 @@
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* This file defines fallback objects to be used during development outside
|
||||
* of the paymentDialogWrapper. When loaded in the wrapper, a frame script
|
||||
* overwrites these methods. Since these methods need to get overwritten in the
|
||||
* global scope, it can't be converted into an ES module.
|
||||
*/
|
||||
|
||||
/* eslint-disable no-console */
|
||||
/* exported log, PaymentDialogUtils */
|
||||
|
||||
"use strict";
|
||||
|
||||
var log = {
|
||||
error: console.error.bind(console, "paymentRequest.xhtml:"),
|
||||
warn: console.warn.bind(console, "paymentRequest.xhtml:"),
|
||||
info: console.info.bind(console, "paymentRequest.xhtml:"),
|
||||
debug: console.debug.bind(console, "paymentRequest.xhtml:"),
|
||||
};
|
||||
|
||||
var PaymentDialogUtils = {
|
||||
getAddressLabel(address, addressFields = null) {
|
||||
if (addressFields) {
|
||||
let requestedFields = addressFields.trim().split(/\s+/);
|
||||
return (
|
||||
requestedFields
|
||||
.filter(f => f && address[f])
|
||||
.map(f => address[f])
|
||||
.join(", ") + ` (${address.guid})`
|
||||
);
|
||||
}
|
||||
return `${address.name} (${address.guid})`;
|
||||
},
|
||||
|
||||
getCreditCardNetworks() {
|
||||
// Shim for list of known and supported credit card network ids as exposed by
|
||||
// toolkit/modules/CreditCard.jsm
|
||||
return [
|
||||
"amex",
|
||||
"cartebancaire",
|
||||
"diners",
|
||||
"discover",
|
||||
"jcb",
|
||||
"mastercard",
|
||||
"mir",
|
||||
"unionpay",
|
||||
"visa",
|
||||
];
|
||||
},
|
||||
isCCNumber(str) {
|
||||
return !!str.replace(/[-\s]/g, "").match(/^\d{9,}$/);
|
||||
},
|
||||
DEFAULT_REGION: "US",
|
||||
countries: new Map([
|
||||
["US", "United States"],
|
||||
["CA", "Canada"],
|
||||
["DE", "Germany"],
|
||||
]),
|
||||
getFormFormat(country) {
|
||||
if (country == "DE") {
|
||||
return {
|
||||
addressLevel3Label: "suburb",
|
||||
addressLevel2Label: "city",
|
||||
addressLevel1Label: "province",
|
||||
addressLevel1Options: null,
|
||||
postalCodeLabel: "postalCode",
|
||||
fieldsOrder: [
|
||||
{
|
||||
fieldId: "name",
|
||||
newLine: true,
|
||||
},
|
||||
{
|
||||
fieldId: "organization",
|
||||
newLine: true,
|
||||
},
|
||||
{
|
||||
fieldId: "street-address",
|
||||
newLine: true,
|
||||
},
|
||||
{ fieldId: "postal-code" },
|
||||
{ fieldId: "address-level2" },
|
||||
],
|
||||
postalCodePattern: "\\d{5}",
|
||||
countryRequiredFields: [
|
||||
"street-address",
|
||||
"address-level2",
|
||||
"postal-code",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
let addressLevel1Options = null;
|
||||
if (country == "US") {
|
||||
addressLevel1Options = new Map([
|
||||
["CA", "California"],
|
||||
["MA", "Massachusetts"],
|
||||
["MI", "Michigan"],
|
||||
]);
|
||||
} else if (country == "CA") {
|
||||
addressLevel1Options = new Map([
|
||||
["NS", "Nova Scotia"],
|
||||
["ON", "Ontario"],
|
||||
["YT", "Yukon"],
|
||||
]);
|
||||
}
|
||||
|
||||
let fieldsOrder = [
|
||||
{ fieldId: "name", newLine: true },
|
||||
{ fieldId: "street-address", newLine: true },
|
||||
{ fieldId: "address-level2" },
|
||||
{ fieldId: "address-level1" },
|
||||
{ fieldId: "postal-code" },
|
||||
{ fieldId: "organization" },
|
||||
];
|
||||
if (country == "BR") {
|
||||
fieldsOrder.splice(2, 0, { fieldId: "address-level3" });
|
||||
}
|
||||
|
||||
return {
|
||||
addressLevel3Label: "suburb",
|
||||
addressLevel2Label: "city",
|
||||
addressLevel1Label: country == "US" ? "state" : "province",
|
||||
addressLevel1Options,
|
||||
postalCodeLabel: country == "US" ? "zip" : "postalCode",
|
||||
fieldsOrder,
|
||||
// The following values come from addressReferences.js and should not be changed.
|
||||
/* eslint-disable-next-line max-len */
|
||||
postalCodePattern:
|
||||
country == "US"
|
||||
? "(\\d{5})(?:[ \\-](\\d{4}))?"
|
||||
: "[ABCEGHJKLMNPRSTVXY]\\d[ABCEGHJ-NPRSTV-Z] ?\\d[ABCEGHJ-NPRSTV-Z]\\d",
|
||||
countryRequiredFields:
|
||||
country == "US" || country == "CA"
|
||||
? [
|
||||
"street-address",
|
||||
"address-level2",
|
||||
"address-level1",
|
||||
"postal-code",
|
||||
]
|
||||
: ["street-address", "address-level2", "postal-code"],
|
||||
};
|
||||
},
|
||||
findAddressSelectOption(selectEl, address, fieldName) {
|
||||
return null;
|
||||
},
|
||||
getDefaultPreferences() {
|
||||
let prefValues = {
|
||||
saveCreditCardDefaultChecked: false,
|
||||
saveAddressDefaultChecked: true,
|
||||
};
|
||||
return prefValues;
|
||||
},
|
||||
isOfficialBranding() {
|
||||
return false;
|
||||
},
|
||||
};
|
@ -1,20 +0,0 @@
|
||||
from __future__ import absolute_import
|
||||
import BaseHTTPServer
|
||||
from SimpleHTTPServer import SimpleHTTPRequestHandler
|
||||
|
||||
|
||||
class RequestHandler(SimpleHTTPRequestHandler, object):
|
||||
def translate_path(self, path):
|
||||
# Map autofill paths to their own directory
|
||||
autofillPath = "/formautofill"
|
||||
if (path.startswith(autofillPath)):
|
||||
path = "browser/extensions/formautofill/content" + \
|
||||
path[len(autofillPath):]
|
||||
else:
|
||||
path = "browser/components/payments/res" + path
|
||||
|
||||
return super(RequestHandler, self).translate_path(path)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
BaseHTTPServer.test(RequestHandler, BaseHTTPServer.HTTPServer)
|
@ -1,612 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/* global info */
|
||||
|
||||
var EXPORTED_SYMBOLS = ["PaymentTestUtils"];
|
||||
|
||||
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 ({ result, delayMs = 0 }) => {
|
||||
let response = await content.showPromise;
|
||||
let completeException;
|
||||
|
||||
// delay the given # milliseconds
|
||||
await new Promise(resolve => content.setTimeout(resolve, delayMs));
|
||||
|
||||
try {
|
||||
await response.complete(result);
|
||||
} catch (ex) {
|
||||
info(`Complete error: ${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,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Add a retry handler to the existing `showPromise` to call .retry().
|
||||
* @returns {Object} representing the PaymentResponse
|
||||
*/
|
||||
addRetryHandler: async ({ validationErrors, delayMs = 0 }) => {
|
||||
let response = await content.showPromise;
|
||||
let retryException;
|
||||
|
||||
// delay the given # milliseconds
|
||||
await new Promise(resolve => content.setTimeout(resolve, delayMs));
|
||||
|
||||
try {
|
||||
await response.retry(Cu.cloneInto(validationErrors, content));
|
||||
} catch (ex) {
|
||||
info(`Retry error: ${ex}`);
|
||||
retryException = {
|
||||
name: ex.name,
|
||||
message: ex.message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
retryException,
|
||||
response: response.toJSON(),
|
||||
// XXX: Bug NNN: workaround for `details` not being included in `toJSON`.
|
||||
methodDetails: response.details,
|
||||
};
|
||||
},
|
||||
|
||||
ensureNoPaymentRequestEvent: ({ eventName }) => {
|
||||
content.rq.addEventListener(eventName, event => {
|
||||
ok(false, `Unexpected ${eventName}`);
|
||||
});
|
||||
},
|
||||
|
||||
promisePaymentRequestEvent: ({ eventName }) => {
|
||||
content[eventName + "Promise"] = new Promise(resolve => {
|
||||
content.rq.addEventListener(eventName, () => {
|
||||
info(`Received event: ${eventName}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Used for PaymentRequest and PaymentResponse event promises.
|
||||
*/
|
||||
awaitPaymentEventPromise: async ({ eventName }) => {
|
||||
await content[eventName + "Promise"];
|
||||
return true;
|
||||
},
|
||||
|
||||
promisePaymentResponseEvent: async ({ eventName }) => {
|
||||
let response = await content.showPromise;
|
||||
content[eventName + "Promise"] = new Promise(resolve => {
|
||||
response.addEventListener(eventName, () => {
|
||||
info(`Received event: ${eventName}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateWith: async ({ eventName, details }) => {
|
||||
/* globals ok */
|
||||
if (
|
||||
details.error &&
|
||||
(!details.shippingOptions || details.shippingOptions.length)
|
||||
) {
|
||||
ok(false, "Need to clear the shipping options to show error text");
|
||||
}
|
||||
if (!details.total) {
|
||||
ok(
|
||||
false,
|
||||
"`total: { label, amount: { value, currency } }` is required for updateWith"
|
||||
);
|
||||
}
|
||||
|
||||
content[eventName + "Promise"] = new Promise(resolve => {
|
||||
content.rq.addEventListener(
|
||||
eventName,
|
||||
event => {
|
||||
event.updateWith(details);
|
||||
resolve();
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new payment request cached as `rq` and then show it.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {PaymentMethodData[]} methodData
|
||||
* @param {PaymentDetailsInit} details
|
||||
* @param {PaymentOptions} options
|
||||
* @returns {Object}
|
||||
*/
|
||||
createAndShowRequest: ({ methodData, details, options }) => {
|
||||
const rq = new content.PaymentRequest(
|
||||
Cu.cloneInto(methodData, content),
|
||||
details,
|
||||
options
|
||||
);
|
||||
content.rq = rq; // assign it so we can retrieve it later
|
||||
|
||||
const handle = content.windowUtils.setHandlingUserInput(true);
|
||||
content.showPromise = rq.show();
|
||||
|
||||
handle.destruct();
|
||||
return {
|
||||
requestId: rq.id,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
DialogContentTasks: {
|
||||
getShippingOptions: () => {
|
||||
let picker = content.document.querySelector("shipping-option-picker");
|
||||
let popupBox = Cu.waiveXrays(picker).dropdown.popupBox;
|
||||
let selectedOptionIndex = popupBox.selectedIndex;
|
||||
let selectedOption = Cu.waiveXrays(picker).dropdown.selectedOption;
|
||||
|
||||
let result = {
|
||||
optionCount: popupBox.children.length,
|
||||
selectedOptionIndex,
|
||||
};
|
||||
if (!selectedOption) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return Object.assign(result, {
|
||||
selectedOptionID: selectedOption.getAttribute("value"),
|
||||
selectedOptionLabel: selectedOption.getAttribute("label"),
|
||||
selectedOptionCurrency: selectedOption.getAttribute("amount-currency"),
|
||||
selectedOptionValue: selectedOption.getAttribute("amount-value"),
|
||||
});
|
||||
},
|
||||
|
||||
getShippingAddresses: () => {
|
||||
let doc = content.document;
|
||||
let addressPicker = doc.querySelector(
|
||||
"address-picker[selected-state-key='selectedShippingAddress']"
|
||||
);
|
||||
let popupBox = Cu.waiveXrays(addressPicker).dropdown.popupBox;
|
||||
let options = Array.from(popupBox.children).map(option => {
|
||||
return {
|
||||
guid: option.getAttribute("guid"),
|
||||
country: option.getAttribute("country"),
|
||||
selected: option.selected,
|
||||
};
|
||||
});
|
||||
let selectedOptionIndex = options.findIndex(item => item.selected);
|
||||
return {
|
||||
selectedOptionIndex,
|
||||
options,
|
||||
};
|
||||
},
|
||||
|
||||
selectShippingAddressByCountry: country => {
|
||||
let doc = content.document;
|
||||
let addressPicker = doc.querySelector(
|
||||
"address-picker[selected-state-key='selectedShippingAddress']"
|
||||
);
|
||||
let select = Cu.waiveXrays(addressPicker).dropdown.popupBox;
|
||||
let option = select.querySelector(`[country="${country}"]`);
|
||||
content.fillField(select, option.value);
|
||||
},
|
||||
|
||||
selectPayerAddressByGuid: guid => {
|
||||
let doc = content.document;
|
||||
let addressPicker = doc.querySelector(
|
||||
"address-picker[selected-state-key='selectedPayerAddress']"
|
||||
);
|
||||
let select = Cu.waiveXrays(addressPicker).dropdown.popupBox;
|
||||
content.fillField(select, guid);
|
||||
},
|
||||
|
||||
selectShippingAddressByGuid: guid => {
|
||||
let doc = content.document;
|
||||
let addressPicker = doc.querySelector(
|
||||
"address-picker[selected-state-key='selectedShippingAddress']"
|
||||
);
|
||||
let select = Cu.waiveXrays(addressPicker).dropdown.popupBox;
|
||||
content.fillField(select, guid);
|
||||
},
|
||||
|
||||
selectShippingOptionById: value => {
|
||||
let doc = content.document;
|
||||
let optionPicker = doc.querySelector("shipping-option-picker");
|
||||
let select = Cu.waiveXrays(optionPicker).dropdown.popupBox;
|
||||
content.fillField(select, value);
|
||||
},
|
||||
|
||||
selectPaymentOptionByGuid: guid => {
|
||||
let doc = content.document;
|
||||
let methodPicker = doc.querySelector("payment-method-picker");
|
||||
let select = Cu.waiveXrays(methodPicker).dropdown.popupBox;
|
||||
content.fillField(select, guid);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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`);
|
||||
ok(
|
||||
!button.disabled,
|
||||
`#${page.id} primary button should not be disabled when clicking it`
|
||||
);
|
||||
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: () => {
|
||||
content.document.getElementById("cancel").click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Do the minimum possible to complete the payment succesfully.
|
||||
*
|
||||
* Don't await on this task since the cancel can close the dialog before
|
||||
* ContentTask can resolve the promise.
|
||||
*
|
||||
* @returns {undefined}
|
||||
*/
|
||||
completePayment: async () => {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Wait for change to payment-summary before clicking Pay"
|
||||
);
|
||||
|
||||
let button = content.document.getElementById("pay");
|
||||
ok(
|
||||
!button.disabled,
|
||||
"Pay button should not be disabled when clicking it"
|
||||
);
|
||||
button.click();
|
||||
},
|
||||
|
||||
setSecurityCode: ({ securityCode }) => {
|
||||
// Waive the xray to access the untrusted `securityCodeInput` property
|
||||
let picker = Cu.waiveXrays(
|
||||
content.document.querySelector("payment-method-picker")
|
||||
);
|
||||
// Unwaive to access the ChromeOnly `setUserInput` API.
|
||||
// setUserInput dispatches changes events.
|
||||
Cu.unwaiveXrays(picker.securityCodeInput)
|
||||
.querySelector("input")
|
||||
.setUserInput(securityCode);
|
||||
},
|
||||
},
|
||||
|
||||
DialogContentUtils: {
|
||||
waitForState: async (content, stateCheckFn, msg) => {
|
||||
const { ContentTaskUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/ContentTaskUtils.jsm"
|
||||
);
|
||||
let { requestStore } = Cu.waiveXrays(
|
||||
content.document.querySelector("payment-dialog")
|
||||
);
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => stateCheckFn(requestStore.getState()),
|
||||
msg
|
||||
);
|
||||
return requestStore.getState();
|
||||
},
|
||||
|
||||
getCurrentState: async content => {
|
||||
let { requestStore } = Cu.waiveXrays(
|
||||
content.document.querySelector("payment-dialog")
|
||||
);
|
||||
return requestStore.getState();
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Common PaymentMethodData for testing
|
||||
*/
|
||||
MethodData: {
|
||||
basicCard: {
|
||||
supportedMethods: "basic-card",
|
||||
},
|
||||
bobPay: {
|
||||
supportedMethods: "https://www.example.com/bobpay",
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Common PaymentDetailsInit for testing
|
||||
*/
|
||||
Details: {
|
||||
total2USD: {
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "USD", value: "2.00" },
|
||||
},
|
||||
},
|
||||
total32USD: {
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "USD", value: "32.00" },
|
||||
},
|
||||
},
|
||||
total60USD: {
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "USD", value: "60.00" },
|
||||
},
|
||||
},
|
||||
total1pt75EUR: {
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "EUR", value: "1.75" },
|
||||
},
|
||||
},
|
||||
total60EUR: {
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "EUR", value: "75.00" },
|
||||
},
|
||||
},
|
||||
twoDisplayItems: {
|
||||
displayItems: [
|
||||
{
|
||||
label: "First",
|
||||
amount: { currency: "USD", value: "1" },
|
||||
},
|
||||
{
|
||||
label: "Second",
|
||||
amount: { currency: "USD", value: "2" },
|
||||
},
|
||||
],
|
||||
},
|
||||
twoDisplayItemsEUR: {
|
||||
displayItems: [
|
||||
{
|
||||
label: "First",
|
||||
amount: { currency: "EUR", value: "0.85" },
|
||||
},
|
||||
{
|
||||
label: "Second",
|
||||
amount: { currency: "EUR", value: "1.70" },
|
||||
},
|
||||
],
|
||||
},
|
||||
twoShippingOptions: {
|
||||
shippingOptions: [
|
||||
{
|
||||
id: "1",
|
||||
label: "Meh Unreliable Shipping",
|
||||
amount: { currency: "USD", value: "1" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Premium Slow Shipping",
|
||||
amount: { currency: "USD", value: "2" },
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
twoShippingOptionsEUR: {
|
||||
shippingOptions: [
|
||||
{
|
||||
id: "1",
|
||||
label: "Sloth Shipping",
|
||||
amount: { currency: "EUR", value: "1.01" },
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
label: "Velociraptor Shipping",
|
||||
amount: { currency: "EUR", value: "63545.65" },
|
||||
selected: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
noShippingOptions: {
|
||||
shippingOptions: [],
|
||||
},
|
||||
bobPayPaymentModifier: {
|
||||
modifiers: [
|
||||
{
|
||||
additionalDisplayItems: [
|
||||
{
|
||||
label: "Credit card fee",
|
||||
amount: { currency: "USD", value: "0.50" },
|
||||
},
|
||||
],
|
||||
supportedMethods: "basic-card",
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "USD", value: "2.50" },
|
||||
},
|
||||
data: {},
|
||||
},
|
||||
{
|
||||
additionalDisplayItems: [
|
||||
{
|
||||
label: "Bob-pay fee",
|
||||
amount: { currency: "USD", value: "1.50" },
|
||||
},
|
||||
],
|
||||
supportedMethods: "https://www.example.com/bobpay",
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "USD", value: "3.50" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
additionalDisplayItemsEUR: {
|
||||
modifiers: [
|
||||
{
|
||||
additionalDisplayItems: [
|
||||
{
|
||||
label: "Handling fee",
|
||||
amount: { currency: "EUR", value: "1.00" },
|
||||
},
|
||||
],
|
||||
supportedMethods: "basic-card",
|
||||
total: {
|
||||
label: "Total due",
|
||||
amount: { currency: "EUR", value: "2.50" },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
noError: {
|
||||
error: "",
|
||||
},
|
||||
genericShippingError: {
|
||||
error: "Cannot ship with option 1 on days that end with Y",
|
||||
},
|
||||
fieldSpecificErrors: {
|
||||
error: "There are errors related to specific parts of the address",
|
||||
shippingAddressErrors: {
|
||||
addressLine:
|
||||
"Can only ship to ROADS, not DRIVES, BOULEVARDS, or STREETS",
|
||||
city: "Can only ship to CITIES, not TOWNSHIPS or VILLAGES",
|
||||
country: "Can only ship to USA, not CA",
|
||||
dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
|
||||
organization: "Can only ship to CORPORATIONS, not CONSORTIUMS",
|
||||
phone: "Only allowed to ship to area codes that start with 9",
|
||||
postalCode: "Only allowed to ship to postalCodes that start with 0",
|
||||
recipient: "Can only ship to names that start with J",
|
||||
region: "Can only ship to regions that start with M",
|
||||
regionCode:
|
||||
"Regions must be 1 to 3 characters in length (sometimes ;) )",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Options: {
|
||||
requestShippingOption: {
|
||||
requestShipping: true,
|
||||
},
|
||||
requestPayerNameAndEmail: {
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: true,
|
||||
},
|
||||
requestPayerNameEmailAndPhone: {
|
||||
requestPayerName: true,
|
||||
requestPayerEmail: true,
|
||||
requestPayerPhone: true,
|
||||
},
|
||||
},
|
||||
|
||||
Addresses: {
|
||||
TimBR: {
|
||||
"given-name": "Timothy",
|
||||
"additional-name": "João",
|
||||
"family-name": "Berners-Lee",
|
||||
organization: "World Wide Web Consortium",
|
||||
"street-address": "Rua Adalberto Pajuaba, 404",
|
||||
"address-level3": "Campos Elísios",
|
||||
"address-level2": "Ribeirão Preto",
|
||||
"address-level1": "SP",
|
||||
"postal-code": "14055-220",
|
||||
country: "BR",
|
||||
tel: "+0318522222222",
|
||||
email: "timbr@example.org",
|
||||
},
|
||||
TimBL: {
|
||||
"given-name": "Timothy",
|
||||
"additional-name": "John",
|
||||
"family-name": "Berners-Lee",
|
||||
organization: "World Wide Web Consortium",
|
||||
"street-address": "32 Vassar Street\nMIT Room 32-G524",
|
||||
"address-level2": "Cambridge",
|
||||
"address-level1": "MA",
|
||||
"postal-code": "02139",
|
||||
country: "US",
|
||||
tel: "+16172535702",
|
||||
email: "timbl@example.org",
|
||||
},
|
||||
TimBL2: {
|
||||
"given-name": "Timothy",
|
||||
"additional-name": "Johann",
|
||||
"family-name": "Berners-Lee",
|
||||
organization: "World Wide Web Consortium",
|
||||
"street-address": "1 Pommes Frittes Place",
|
||||
"address-level2": "Berlin",
|
||||
// address-level1 isn't used in our forms for Germany
|
||||
"postal-code": "02138",
|
||||
country: "DE",
|
||||
tel: "+16172535702",
|
||||
email: "timbl@example.com",
|
||||
},
|
||||
/* Used as a temporary (not persisted in autofill storage) address in tests */
|
||||
Temp: {
|
||||
"given-name": "Temp",
|
||||
"family-name": "McTempFace",
|
||||
organization: "Temps Inc.",
|
||||
"street-address": "1a Temporary Ave.",
|
||||
"address-level2": "Temp Town",
|
||||
"address-level1": "CA",
|
||||
"postal-code": "31337",
|
||||
country: "US",
|
||||
tel: "+15032541000",
|
||||
email: "tempie@example.com",
|
||||
},
|
||||
},
|
||||
|
||||
BasicCards: {
|
||||
JohnDoe: {
|
||||
"cc-exp-month": 1,
|
||||
"cc-exp-year": new Date().getFullYear() + 9,
|
||||
"cc-name": "John Doe",
|
||||
"cc-number": "4111111111111111",
|
||||
"cc-type": "visa",
|
||||
},
|
||||
JaneMasterCard: {
|
||||
"cc-exp-month": 12,
|
||||
"cc-exp-year": new Date().getFullYear() + 9,
|
||||
"cc-name": "Jane McMaster-Card",
|
||||
"cc-number": "5555555555554444",
|
||||
"cc-type": "mastercard",
|
||||
},
|
||||
MissingFields: {
|
||||
"cc-name": "Missy Fields",
|
||||
"cc-number": "340000000000009",
|
||||
},
|
||||
Temp: {
|
||||
"cc-exp-month": 12,
|
||||
"cc-exp-year": new Date().getFullYear() + 9,
|
||||
"cc-name": "Temp Name",
|
||||
"cc-number": "5105105105105100",
|
||||
"cc-type": "mastercard",
|
||||
},
|
||||
},
|
||||
};
|
@ -1,10 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Blank page</title>
|
||||
</head>
|
||||
<body>
|
||||
BLANK PAGE
|
||||
</body>
|
||||
</html>
|
@ -1,34 +0,0 @@
|
||||
[DEFAULT]
|
||||
head = head.js
|
||||
prefs =
|
||||
browser.pagethumbnails.capturing_disabled=true
|
||||
dom.payments.request.enabled=true
|
||||
extensions.formautofill.creditCards.available=true
|
||||
extensions.formautofill.reauth.enabled=true
|
||||
skip-if = true || !e10s # Bug 1515048 - Disable for now. Bug 1365964 - Payment Request isn't implemented for non-e10s
|
||||
support-files =
|
||||
blank_page.html
|
||||
|
||||
[browser_address_edit.js]
|
||||
skip-if = verify && debug && os == 'mac'
|
||||
[browser_address_edit_hidden_fields.js]
|
||||
[browser_card_edit.js]
|
||||
skip-if = debug && (os == 'mac' || os == 'linux') # bug 1465673
|
||||
[browser_change_shipping.js]
|
||||
[browser_dropdowns.js]
|
||||
[browser_host_name.js]
|
||||
[browser_onboarding_wizard.js]
|
||||
[browser_openPreferences.js]
|
||||
[browser_payerRequestedFields.js]
|
||||
[browser_payment_completion.js]
|
||||
[browser_profile_storage.js]
|
||||
[browser_request_serialization.js]
|
||||
[browser_request_shipping.js]
|
||||
[browser_retry.js]
|
||||
[browser_retry_fieldErrors.js]
|
||||
[browser_shippingaddresschange_error.js]
|
||||
[browser_show_dialog.js]
|
||||
skip-if = os == 'win' && debug # bug 1418385
|
||||
[browser_tab_modal.js]
|
||||
skip-if = os == 'linux' && !debug # bug 1508577
|
||||
[browser_total.js]
|
File diff suppressed because it is too large
Load Diff
@ -1,473 +0,0 @@
|
||||
/**
|
||||
* Test saving/updating address records with fields sometimes not visible to the user.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
async function setup() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
// add an address and card to avoid the FTU sequence
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBL],
|
||||
[PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
|
||||
info("associating the card with the billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
prefilledGuids.card1GUID,
|
||||
{
|
||||
billingAddressGUID: prefilledGuids.address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return prefilledGuids;
|
||||
}
|
||||
|
||||
add_task(async function test_hiddenFieldNotSaved() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let newAddress = Object.assign({}, PTU.Addresses.TimBL);
|
||||
newAddress["given-name"] = "hiddenFields";
|
||||
|
||||
let shippingAddressChangePromise = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
let options = {
|
||||
addLinkSelector: "address-picker.shipping-related .add-link",
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "shipping-address-page",
|
||||
expectPersist: true,
|
||||
isEditing: false,
|
||||
};
|
||||
await navigateToAddAddressPage(frame, options);
|
||||
info("navigated to add address page");
|
||||
await fillInShippingAddressForm(frame, newAddress, options);
|
||||
info("filled in TimBL address");
|
||||
|
||||
await spawnPaymentDialogTask(frame, async args => {
|
||||
let addressForm = content.document.getElementById(
|
||||
"shipping-address-page"
|
||||
);
|
||||
let countryField = addressForm.querySelector("#country");
|
||||
await content.fillField(countryField, "DE");
|
||||
});
|
||||
info("changed selected country to Germany");
|
||||
|
||||
await submitAddressForm(frame, null, options);
|
||||
await shippingAddressChangePromise;
|
||||
info("got shippingaddresschange event");
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let { savedAddresses } = await PTU.DialogContentUtils.getCurrentState(
|
||||
content
|
||||
);
|
||||
is(Object.keys(savedAddresses).length, 2, "2 saved addresses");
|
||||
let savedAddress = Object.values(savedAddresses).find(
|
||||
address => address["given-name"] == "hiddenFields"
|
||||
);
|
||||
ok(savedAddress, "found the saved address");
|
||||
is(savedAddress.country, "DE", "check country");
|
||||
is(
|
||||
savedAddress["address-level2"],
|
||||
PTU.Addresses.TimBL["address-level2"],
|
||||
"check address-level2"
|
||||
);
|
||||
is(
|
||||
savedAddress["address-level1"],
|
||||
undefined,
|
||||
"address-level1 should not be saved"
|
||||
);
|
||||
});
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_hiddenFieldRemovedWhenCountryChanged() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let shippingAddressChangePromise = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async args => {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let picker = content.document.querySelector(
|
||||
"address-picker[selected-state-key='selectedShippingAddress']"
|
||||
);
|
||||
Cu.waiveXrays(picker).dropdown.popupBox.focus();
|
||||
EventUtils.synthesizeKey(
|
||||
PTU.Addresses.TimBL["given-name"],
|
||||
{},
|
||||
content.window
|
||||
);
|
||||
|
||||
let editLink = content.document.querySelector(
|
||||
"address-picker .edit-link"
|
||||
);
|
||||
is(editLink.textContent, "Edit", "Edit link text");
|
||||
|
||||
editLink.click();
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "shipping-address-page" &&
|
||||
!!state["shipping-address-page"].guid
|
||||
);
|
||||
},
|
||||
"Check edit page state"
|
||||
);
|
||||
|
||||
let addressForm = content.document.getElementById(
|
||||
"shipping-address-page"
|
||||
);
|
||||
let countryField = addressForm.querySelector("#country");
|
||||
await content.fillField(countryField, "DE");
|
||||
info("changed selected country to Germany");
|
||||
});
|
||||
|
||||
let options = {
|
||||
isEditing: true,
|
||||
addressPageId: "shipping-address-page",
|
||||
};
|
||||
await submitAddressForm(frame, null, options);
|
||||
await shippingAddressChangePromise;
|
||||
info("got shippingaddresschange event");
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let { savedAddresses } = await PTU.DialogContentUtils.getCurrentState(
|
||||
content
|
||||
);
|
||||
is(Object.keys(savedAddresses).length, 1, "1 saved address");
|
||||
let savedAddress = Object.values(savedAddresses)[0];
|
||||
is(savedAddress.country, "DE", "check country");
|
||||
is(
|
||||
savedAddress["address-level2"],
|
||||
PTU.Addresses.TimBL["address-level2"],
|
||||
"check address-level2"
|
||||
);
|
||||
is(
|
||||
savedAddress["address-level1"],
|
||||
undefined,
|
||||
"address-level1 should not be saved"
|
||||
);
|
||||
});
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_hiddenNonShippingFieldsPreservedUponEdit() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
// add a Brazilian address (since it uses all fields) and card to avoid the FTU sequence
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBR],
|
||||
[PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
let expected = await formAutofillStorage.addresses.get(
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "BR");
|
||||
|
||||
await navigateToAddShippingAddressPage(frame, {
|
||||
addLinkSelector:
|
||||
'address-picker[selected-state-key="selectedShippingAddress"] .edit-link',
|
||||
addressPageId: "shipping-address-page",
|
||||
initialPageId: "payment-summary",
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let givenNameField = content.document.querySelector(
|
||||
"#shipping-address-page #given-name"
|
||||
);
|
||||
await content.fillField(givenNameField, "Timothy-edit");
|
||||
});
|
||||
|
||||
let options = {
|
||||
isEditing: true,
|
||||
};
|
||||
await submitAddressForm(frame, null, options);
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Object.assign(expected, PTU.Addresses.TimBR, {
|
||||
"given-name": "Timothy-edit",
|
||||
name: "Timothy-edit Jo\u{00E3}o Berners-Lee",
|
||||
});
|
||||
let actual = await formAutofillStorage.addresses.get(
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
isnot(actual.email, "", "Check email isn't empty");
|
||||
// timeLastModified changes and isn't relevant
|
||||
delete actual.timeLastModified;
|
||||
delete expected.timeLastModified;
|
||||
SimpleTest.isDeeply(actual, expected, "Check profile didn't lose fields");
|
||||
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_hiddenNonPayerFieldsPreservedUponEdit() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
// add a Brazilian address (since it uses all fields) and card to avoid the FTU sequence
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBR],
|
||||
[PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
let expected = await formAutofillStorage.addresses.get(
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
|
||||
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.total2USD),
|
||||
options: {
|
||||
requestPayerEmail: true,
|
||||
},
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await navigateToAddAddressPage(frame, {
|
||||
addLinkSelector:
|
||||
'address-picker[selected-state-key="selectedPayerAddress"] .edit-link',
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "payer-address-page",
|
||||
});
|
||||
|
||||
info("Change the email address");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
`async () => {
|
||||
let emailField = content.document.querySelector("#payer-address-page #email");
|
||||
await content.fillField(emailField, "new@example.com");
|
||||
}`
|
||||
);
|
||||
|
||||
let options = {
|
||||
isEditing: true,
|
||||
};
|
||||
await submitAddressForm(frame, null, options);
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Object.assign(expected, PTU.Addresses.TimBR, {
|
||||
email: "new@example.com",
|
||||
});
|
||||
let actual = await formAutofillStorage.addresses.get(
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
// timeLastModified changes and isn't relevant
|
||||
delete actual.timeLastModified;
|
||||
delete expected.timeLastModified;
|
||||
SimpleTest.isDeeply(
|
||||
actual,
|
||||
expected,
|
||||
"Check profile didn't lose fields and change was made"
|
||||
);
|
||||
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_hiddenNonBillingAddressFieldsPreservedUponEdit() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
// add a Brazilian address (since it uses all fields) and card to avoid the FTU sequence
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBR],
|
||||
[PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
let expected = await formAutofillStorage.addresses.get(
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
|
||||
info("associating the card with the billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
prefilledGuids.card1GUID,
|
||||
{
|
||||
billingAddressGUID: prefilledGuids.address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await navigateToAddCardPage(frame, {
|
||||
addLinkSelector: "payment-method-picker .edit-link",
|
||||
});
|
||||
|
||||
await navigateToAddAddressPage(frame, {
|
||||
addLinkSelector: ".billingAddressRow .edit-link",
|
||||
initialPageId: "basic-card-page",
|
||||
addressPageId: "billing-address-page",
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
`async () => {
|
||||
let givenNameField = content.document.querySelector(
|
||||
"#billing-address-page #given-name"
|
||||
);
|
||||
await content.fillField(givenNameField, "Timothy-edit");
|
||||
}`
|
||||
);
|
||||
|
||||
let options = {
|
||||
isEditing: true,
|
||||
nextPageId: "basic-card-page",
|
||||
};
|
||||
await submitAddressForm(frame, null, options);
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Object.assign(expected, PTU.Addresses.TimBR, {
|
||||
"given-name": "Timothy-edit",
|
||||
name: "Timothy-edit Jo\u{00E3}o Berners-Lee",
|
||||
});
|
||||
let actual = await formAutofillStorage.addresses.get(
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
// timeLastModified changes and isn't relevant
|
||||
delete actual.timeLastModified;
|
||||
delete expected.timeLastModified;
|
||||
SimpleTest.isDeeply(actual, expected, "Check profile didn't lose fields");
|
||||
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
File diff suppressed because it is too large
Load Diff
@ -1,712 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
async function setup() {
|
||||
await setupFormAutofillStorage();
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard();
|
||||
|
||||
info("associating the card with the billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
prefilledGuids.card1GUID,
|
||||
{
|
||||
billingAddressGUID: prefilledGuids.address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return prefilledGuids;
|
||||
}
|
||||
|
||||
add_task(async function test_change_shipping() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
let shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionCurrency,
|
||||
"USD",
|
||||
"Shipping options should be in USD"
|
||||
);
|
||||
is(
|
||||
shippingOptions.optionCount,
|
||||
2,
|
||||
"there should be two shipping options"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionID,
|
||||
"2",
|
||||
"default selected should be '2'"
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectShippingOptionById,
|
||||
"1"
|
||||
);
|
||||
|
||||
shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.optionCount,
|
||||
2,
|
||||
"there should be two shipping options"
|
||||
);
|
||||
is(shippingOptions.selectedOptionID, "1", "selected should be '1'");
|
||||
|
||||
let paymentDetails = Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptionsEUR,
|
||||
PTU.Details.total1pt75EUR,
|
||||
PTU.Details.twoDisplayItemsEUR,
|
||||
PTU.Details.additionalDisplayItemsEUR
|
||||
);
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: paymentDetails,
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
info("added shipping change handler to change to EUR");
|
||||
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "DE");
|
||||
info("changed shipping address to DE country");
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
info("got shippingaddresschange event");
|
||||
|
||||
// verify update of shippingOptions
|
||||
shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionCurrency,
|
||||
"EUR",
|
||||
"Shipping options should be in EUR after the shippingaddresschange"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionID,
|
||||
"1",
|
||||
"id:1 should still be selected"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionValue,
|
||||
"1.01",
|
||||
"amount should be '1.01' after the shippingaddresschange"
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
// verify update of total
|
||||
// Note: The update includes a modifier, and modifiers must include a total
|
||||
// so the expected total is that one
|
||||
is(
|
||||
content.document.querySelector("#total > currency-amount")
|
||||
.textContent,
|
||||
"\u20AC2.50 EUR",
|
||||
"Check updated total currency amount"
|
||||
);
|
||||
|
||||
let btn = content.document.querySelector("#view-all");
|
||||
btn.click();
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.orderDetailsShowing;
|
||||
},
|
||||
"Order details show be showing now"
|
||||
);
|
||||
|
||||
let container = content.document.querySelector("order-details");
|
||||
let items = [
|
||||
...container.querySelectorAll(".main-list payment-details-item"),
|
||||
].map(item => Cu.waiveXrays(item));
|
||||
|
||||
// verify the updated displayItems
|
||||
is(items.length, 2, "2 display items");
|
||||
is(items[0].amountCurrency, "EUR", "First display item is in Euros");
|
||||
is(items[1].amountCurrency, "EUR", "2nd display item is in Euros");
|
||||
is(items[0].amountValue, "0.85", "First display item has 0.85 value");
|
||||
is(items[1].amountValue, "1.70", "2nd display item has 1.70 value");
|
||||
|
||||
// verify the updated modifiers
|
||||
items = [
|
||||
...container.querySelectorAll(
|
||||
".footer-items-list payment-details-item"
|
||||
),
|
||||
].map(item => Cu.waiveXrays(item));
|
||||
is(items.length, 1, "1 additional display item");
|
||||
is(items[0].amountCurrency, "EUR", "First display item is in Euros");
|
||||
is(items[0].amountValue, "1.00", "First display item has 1.00 value");
|
||||
btn.click();
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking pay");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
is(result.response.methodName, "basic-card", "Check methodName");
|
||||
|
||||
let { shippingAddress } = result.response;
|
||||
let expectedAddress = PTU.Addresses.TimBL2;
|
||||
checkPaymentAddressMatchesStorageAddress(
|
||||
shippingAddress,
|
||||
expectedAddress,
|
||||
"Shipping address"
|
||||
);
|
||||
|
||||
let { methodDetails } = result;
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
methodDetails,
|
||||
PTU.BasicCards.JohnDoe,
|
||||
"Payment method"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_default_shippingOptions_noneSelected() {
|
||||
await setup();
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let shippingOptionDetails = Object.assign(
|
||||
deepClone(PTU.Details.twoShippingOptions),
|
||||
PTU.Details.total2USD
|
||||
);
|
||||
info("make sure no shipping options are selected");
|
||||
shippingOptionDetails.shippingOptions.forEach(opt => delete opt.selected);
|
||||
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: shippingOptionDetails,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.optionCount,
|
||||
2,
|
||||
"there should be two shipping options"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionIndex,
|
||||
"-1",
|
||||
"no options should be selected"
|
||||
);
|
||||
|
||||
let shippingOptionDetailsEUR = deepClone(
|
||||
PTU.Details.twoShippingOptionsEUR
|
||||
);
|
||||
info("prepare EUR options by deselecting all and giving unique IDs");
|
||||
shippingOptionDetailsEUR.shippingOptions.forEach(opt => {
|
||||
opt.selected = false;
|
||||
opt.id += "-EUR";
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectShippingOptionById,
|
||||
"1"
|
||||
);
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: Object.assign(
|
||||
shippingOptionDetailsEUR,
|
||||
PTU.Details.total1pt75EUR
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
info("added shipping change handler to change to EUR");
|
||||
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "DE");
|
||||
info("changed shipping address to DE country");
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
info("got shippingaddresschange event");
|
||||
|
||||
shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.optionCount,
|
||||
2,
|
||||
"there should be two shipping options"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionIndex,
|
||||
"-1",
|
||||
"no options should be selected again"
|
||||
);
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_default_shippingOptions_allSelected() {
|
||||
await setup();
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let shippingOptionDetails = Object.assign(
|
||||
deepClone(PTU.Details.twoShippingOptions),
|
||||
PTU.Details.total2USD
|
||||
);
|
||||
info("make sure no shipping options are selected");
|
||||
shippingOptionDetails.shippingOptions.forEach(
|
||||
opt => (opt.selected = true)
|
||||
);
|
||||
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: shippingOptionDetails,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionCurrency,
|
||||
"USD",
|
||||
"Shipping options should be in USD"
|
||||
);
|
||||
is(
|
||||
shippingOptions.optionCount,
|
||||
2,
|
||||
"there should be two shipping options"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionID,
|
||||
"2",
|
||||
"default selected should be the last selected=true"
|
||||
);
|
||||
|
||||
let shippingOptionDetailsEUR = deepClone(
|
||||
PTU.Details.twoShippingOptionsEUR
|
||||
);
|
||||
info("prepare EUR options by selecting all and giving unique IDs");
|
||||
shippingOptionDetailsEUR.shippingOptions.forEach(opt => {
|
||||
opt.selected = true;
|
||||
opt.id += "-EUR";
|
||||
});
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: Object.assign(
|
||||
shippingOptionDetailsEUR,
|
||||
PTU.Details.total1pt75EUR
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
info("added shipping change handler to change to EUR");
|
||||
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "DE");
|
||||
info("changed shipping address to DE country");
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
info("got shippingaddresschange event");
|
||||
|
||||
shippingOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingOptions
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionCurrency,
|
||||
"EUR",
|
||||
"Shipping options should be in EUR"
|
||||
);
|
||||
is(
|
||||
shippingOptions.optionCount,
|
||||
2,
|
||||
"there should be two shipping options"
|
||||
);
|
||||
is(
|
||||
shippingOptions.selectedOptionID,
|
||||
"2-EUR",
|
||||
"default selected should be the last selected=true"
|
||||
);
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_no_shippingchange_without_shipping() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.ensureNoPaymentRequestEvent
|
||||
);
|
||||
info("added shipping change handler");
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "456",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking pay");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
is(result.response.methodName, "basic-card", "Check methodName");
|
||||
|
||||
let actualShippingAddress = result.response.shippingAddress;
|
||||
ok(
|
||||
actualShippingAddress === null,
|
||||
"Check that shipping address is null with requestShipping:false"
|
||||
);
|
||||
|
||||
let { methodDetails } = result;
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
methodDetails,
|
||||
PTU.BasicCards.JohnDoe,
|
||||
"Payment method"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_address_edit() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
});
|
||||
|
||||
let addressOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingAddresses
|
||||
);
|
||||
info("initial addressOptions: " + JSON.stringify(addressOptions));
|
||||
let selectedIndex = addressOptions.selectedOptionIndex;
|
||||
|
||||
is(selectedIndex, -1, "No address should be selected initially");
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.promisePaymentRequestEvent
|
||||
);
|
||||
|
||||
info("selecting the US address");
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "US");
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
addressOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingAddresses
|
||||
);
|
||||
info("initial addressOptions: " + JSON.stringify(addressOptions));
|
||||
selectedIndex = addressOptions.selectedOptionIndex;
|
||||
let selectedAddressGuid = addressOptions.options[selectedIndex].guid;
|
||||
let selectedAddress = await formAutofillStorage.addresses.get(
|
||||
selectedAddressGuid
|
||||
);
|
||||
|
||||
// US address is inserted first, then German address, so German address
|
||||
// has more recent timeLastModified and will appear at the top of the list.
|
||||
is(selectedIndex, 1, "Second address should be selected");
|
||||
ok(
|
||||
selectedAddress,
|
||||
"Selected address does exist in the address collection"
|
||||
);
|
||||
is(selectedAddress.country, "US", "Expected initial country value");
|
||||
|
||||
info("Updating the address directly in the store");
|
||||
await formAutofillStorage.addresses.update(
|
||||
selectedAddress.guid,
|
||||
{
|
||||
country: "CA",
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
addressOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingAddresses
|
||||
);
|
||||
info("updated addressOptions: " + JSON.stringify(addressOptions));
|
||||
selectedIndex = addressOptions.selectedOptionIndex;
|
||||
let newSelectedAddressGuid = addressOptions.options[selectedIndex].guid;
|
||||
|
||||
is(
|
||||
newSelectedAddressGuid,
|
||||
selectedAddressGuid,
|
||||
"Selected guid hasnt changed"
|
||||
);
|
||||
selectedAddress = await formAutofillStorage.addresses.get(
|
||||
selectedAddressGuid
|
||||
);
|
||||
|
||||
is(selectedIndex, 1, "Second address should be selected");
|
||||
is(selectedAddress.country, "CA", "Expected changed country value");
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
||||
|
||||
add_task(async function test_address_removal() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
});
|
||||
|
||||
info("selecting the US address");
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "US");
|
||||
|
||||
let addressOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingAddresses
|
||||
);
|
||||
info("initial addressOptions: " + JSON.stringify(addressOptions));
|
||||
let selectedIndex = addressOptions.selectedOptionIndex;
|
||||
let selectedAddressGuid = addressOptions.options[selectedIndex].guid;
|
||||
|
||||
// US address is inserted first, then German address, so German address
|
||||
// has more recent timeLastModified and will appear at the top of the list.
|
||||
is(selectedIndex, 1, "Second address should be selected");
|
||||
is(
|
||||
addressOptions.options.length,
|
||||
2,
|
||||
"Should be 2 address options initially"
|
||||
);
|
||||
|
||||
info("Remove the selected address from the store");
|
||||
await formAutofillStorage.addresses.remove(selectedAddressGuid);
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.promisePaymentRequestEvent
|
||||
);
|
||||
|
||||
addressOptions = await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.getShippingAddresses
|
||||
);
|
||||
info("updated addressOptions: " + JSON.stringify(addressOptions));
|
||||
selectedIndex = addressOptions.selectedOptionIndex;
|
||||
|
||||
is(
|
||||
selectedIndex,
|
||||
-1,
|
||||
"No replacement address should be selected after deletion"
|
||||
);
|
||||
is(addressOptions.options.length, 1, "Should now be 1 address option");
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
@ -1,82 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_dropdown() {
|
||||
await addSampleAddressesAndBasicCard();
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
details: PTU.Details.total60USD,
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let popupset = frame.ownerDocument.querySelector("popupset");
|
||||
ok(popupset, "popupset exists");
|
||||
let popupshownPromise = BrowserTestUtils.waitForEvent(
|
||||
popupset,
|
||||
"popupshown"
|
||||
);
|
||||
|
||||
info("switch to the address add page");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function changeToAddressAddPage() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let addLink = content.document.querySelector(
|
||||
"address-picker.shipping-related .add-link"
|
||||
);
|
||||
is(addLink.textContent, "Add", "Add link text");
|
||||
|
||||
addLink.click();
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "shipping-address-page" && !state.page.guid
|
||||
);
|
||||
},
|
||||
"Check add page state"
|
||||
);
|
||||
|
||||
content.document
|
||||
.querySelector("#shipping-address-page #country")
|
||||
.scrollIntoView();
|
||||
}
|
||||
);
|
||||
|
||||
info("going to open the country <select>");
|
||||
await BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
"#shipping-address-page #country",
|
||||
{},
|
||||
frame
|
||||
);
|
||||
|
||||
let event = await popupshownPromise;
|
||||
let expectedPopupID = "ContentSelectDropdown";
|
||||
is(
|
||||
event.target.parentElement.id,
|
||||
expectedPopupID,
|
||||
"Checked menulist of opened popup"
|
||||
);
|
||||
|
||||
event.target.hidePopup(true);
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,50 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
async function withBasicRequestDialogForOrigin(origin, dialogTaskFn) {
|
||||
const args = {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
dialogTaskFn,
|
||||
args,
|
||||
{
|
||||
origin,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
add_task(async function test_host() {
|
||||
await withBasicRequestDialogForOrigin("https://example.com", () => {
|
||||
is(
|
||||
content.document.querySelector("#host-name").textContent,
|
||||
"example.com",
|
||||
"Check basic host name"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_host_subdomain() {
|
||||
await withBasicRequestDialogForOrigin("https://test1.example.com", () => {
|
||||
is(
|
||||
content.document.querySelector("#host-name").textContent,
|
||||
"test1.example.com",
|
||||
"Check host name with subdomain"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_host_IDN() {
|
||||
await withBasicRequestDialogForOrigin(
|
||||
"https://xn--hxajbheg2az3al.xn--jxalpdlp",
|
||||
() => {
|
||||
is(
|
||||
content.document.querySelector("#host-name").textContent,
|
||||
"\u03C0\u03B1\u03C1\u03AC\u03B4\u03B5\u03B9\u03B3\u03BC\u03B1." +
|
||||
"\u03B4\u03BF\u03BA\u03B9\u03BC\u03AE",
|
||||
"Check IDN domain"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,859 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
async function addAddress() {
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
formAutofillStorage.addresses.add(PTU.Addresses.TimBL);
|
||||
await onChanged;
|
||||
}
|
||||
|
||||
async function addBasicCard() {
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
formAutofillStorage.creditCards.add(PTU.BasicCards.JohnDoe);
|
||||
await onChanged;
|
||||
}
|
||||
|
||||
add_task(
|
||||
async function test_onboarding_wizard_without_saved_addresses_and_saved_cards() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
cleanupFormAutofillStorage();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "shipping-address-page";
|
||||
},
|
||||
"Address page is shown first during on-boarding if there are no saved addresses"
|
||||
);
|
||||
|
||||
let addressForm = content.document.querySelector(
|
||||
"#shipping-address-page"
|
||||
);
|
||||
info("Checking if the address page has been rendered");
|
||||
let addressSaveButton = addressForm.querySelector(".save-button");
|
||||
ok(
|
||||
content.isVisible(addressSaveButton),
|
||||
"Address save button is rendered"
|
||||
);
|
||||
is(
|
||||
addressSaveButton.textContent,
|
||||
"Next",
|
||||
"Address save button has the correct label during onboarding"
|
||||
);
|
||||
|
||||
info(
|
||||
"Check if the total header is visible on the address page during on-boarding"
|
||||
);
|
||||
let header = content.document.querySelector("header");
|
||||
ok(
|
||||
content.isVisible(header),
|
||||
"Total Header is visible on the address page during on-boarding"
|
||||
);
|
||||
ok(header.textContent, "Total Header contains text");
|
||||
|
||||
info("Check if the page title is visible on the address page");
|
||||
let addressPageTitle = addressForm.querySelector("h2");
|
||||
ok(
|
||||
content.isVisible(addressPageTitle),
|
||||
"Address page title is visible"
|
||||
);
|
||||
is(
|
||||
addressPageTitle.textContent,
|
||||
"Add Shipping Address",
|
||||
"Address page title is correctly shown"
|
||||
);
|
||||
|
||||
let addressCancelButton = addressForm.querySelector(".cancel-button");
|
||||
ok(
|
||||
content.isVisible(addressCancelButton),
|
||||
"The cancel button on the address page is visible"
|
||||
);
|
||||
});
|
||||
|
||||
let addOptions = {
|
||||
addLinkSelector: "address-picker.shipping-related .add-link",
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "shipping-address-page",
|
||||
};
|
||||
|
||||
await fillInShippingAddressForm(
|
||||
frame,
|
||||
PTU.Addresses.TimBL2,
|
||||
addOptions
|
||||
);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
},
|
||||
"Basic card page is shown after the address page during on boarding"
|
||||
);
|
||||
|
||||
let cardSaveButton = content.document.querySelector(
|
||||
"basic-card-form .save-button"
|
||||
);
|
||||
is(
|
||||
cardSaveButton.textContent,
|
||||
"Next",
|
||||
"Card save button has the correct label during onboarding"
|
||||
);
|
||||
ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
|
||||
|
||||
let basicCardTitle = content.document.querySelector(
|
||||
"basic-card-form h2"
|
||||
);
|
||||
ok(
|
||||
content.isVisible(basicCardTitle),
|
||||
"Basic card page title is visible"
|
||||
);
|
||||
is(
|
||||
basicCardTitle.textContent,
|
||||
"Add Credit Card",
|
||||
"Basic card page title is correctly shown"
|
||||
);
|
||||
|
||||
info(
|
||||
"Check if the correct billing address is selected in the basic card page"
|
||||
);
|
||||
PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
let billingAddressSelect = content.document.querySelector(
|
||||
"#billingAddressGUID"
|
||||
);
|
||||
return (
|
||||
state.selectedShippingAddress == billingAddressSelect.value
|
||||
);
|
||||
},
|
||||
"Shipping address is selected as the billing address"
|
||||
);
|
||||
});
|
||||
|
||||
await fillInCardForm(frame, {
|
||||
["cc-csc"]: "123",
|
||||
...PTU.BasicCards.JohnDoe,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Payment summary page is shown after the basic card page during on boarding"
|
||||
);
|
||||
|
||||
let cancelButton = content.document.querySelector("#cancel");
|
||||
ok(
|
||||
content.isVisible(cancelButton),
|
||||
"Payment summary page is rendered"
|
||||
);
|
||||
});
|
||||
|
||||
info("Closing the payment dialog");
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_onboarding_wizard_with_saved_addresses_and_saved_cards() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
addSampleAddressesAndBasicCard();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Payment summary page is shown first when there are saved addresses and saved cards"
|
||||
);
|
||||
|
||||
info("Checking if the payment summary page is now visible");
|
||||
let cancelButton = content.document.querySelector("#cancel");
|
||||
ok(
|
||||
content.isVisible(cancelButton),
|
||||
"Payment summary page is rendered"
|
||||
);
|
||||
|
||||
info("Check if the total header is visible on payments summary page");
|
||||
let header = content.document.querySelector("header");
|
||||
ok(
|
||||
content.isVisible(header),
|
||||
"Total Header is visible on the payment summary page"
|
||||
);
|
||||
ok(header.textContent, "Total Header contains text");
|
||||
|
||||
// Click on the Add/Edit buttons in the payment summary page to check if
|
||||
// the total header is visible on the address page and the basic card page.
|
||||
let buttons = [
|
||||
"address-picker[selected-state-key='selectedShippingAddress'] .add-link",
|
||||
"address-picker[selected-state-key='selectedShippingAddress'] .edit-link",
|
||||
"payment-method-picker .add-link",
|
||||
"payment-method-picker .edit-link",
|
||||
];
|
||||
for (let button of buttons) {
|
||||
content.document.querySelector(button).click();
|
||||
if (button.startsWith("address")) {
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "shipping-address-page";
|
||||
},
|
||||
"Shipping address page is shown"
|
||||
);
|
||||
} else {
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
},
|
||||
"Basic card page is shown"
|
||||
);
|
||||
}
|
||||
|
||||
ok(
|
||||
!content.isVisible(header),
|
||||
"Total Header is hidden on the address/basic card page"
|
||||
);
|
||||
|
||||
if (button.startsWith("address")) {
|
||||
content.document
|
||||
.querySelector("#shipping-address-page .back-button")
|
||||
.click();
|
||||
} else {
|
||||
content.document
|
||||
.querySelector("basic-card-form .back-button")
|
||||
.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info("Closing the payment dialog");
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
|
||||
cleanupFormAutofillStorage();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_onboarding_wizard_with_saved_addresses_and_no_saved_cards() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
addAddress();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
},
|
||||
"Basic card page is shown first if there are saved addresses during on boarding"
|
||||
);
|
||||
|
||||
info("Checking if the basic card page has been rendered");
|
||||
let cardSaveButton = content.document.querySelector(
|
||||
"basic-card-form .save-button"
|
||||
);
|
||||
ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
|
||||
|
||||
info(
|
||||
"Check if the total header is visible on the basic card page during on-boarding"
|
||||
);
|
||||
let header = content.document.querySelector("header");
|
||||
ok(
|
||||
content.isVisible(header),
|
||||
"Total Header is visible on the basic card page during on-boarding"
|
||||
);
|
||||
ok(header.textContent, "Total Header contains text");
|
||||
|
||||
let cardCancelButton = content.document.querySelector(
|
||||
"basic-card-form .cancel-button"
|
||||
);
|
||||
ok(
|
||||
content.isVisible(cardCancelButton),
|
||||
"Cancel button is visible on the basic card page"
|
||||
);
|
||||
|
||||
let cardBackButton = content.document.querySelector(
|
||||
"basic-card-form .back-button"
|
||||
);
|
||||
ok(
|
||||
!content.isVisible(cardBackButton),
|
||||
"Back button is hidden on the basic card page when it is shown first during onboarding"
|
||||
);
|
||||
});
|
||||
|
||||
// Do not await for this task since the dialog may close before the task resolves.
|
||||
spawnPaymentDialogTask(frame, () => {
|
||||
content.document
|
||||
.querySelector("basic-card-form .cancel-button")
|
||||
.click();
|
||||
});
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
|
||||
cleanupFormAutofillStorage();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_onboarding_wizard_without_saved_address_with_saved_cards() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
cleanupFormAutofillStorage();
|
||||
addBasicCard();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let addOptions = {
|
||||
addLinkSelector: "address-picker.shipping-related .add-link",
|
||||
checkboxSelector: "#shipping-address-page .persist-checkbox",
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "shipping-address-page",
|
||||
expectPersist: true,
|
||||
};
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "shipping-address-page";
|
||||
},
|
||||
"Shipping address page is shown first if there are saved addresses during on boarding"
|
||||
);
|
||||
|
||||
info("Checking if the address page has been rendered");
|
||||
let addressForm = content.document.querySelector(
|
||||
"#shipping-address-page"
|
||||
);
|
||||
let addressSaveButton = addressForm.querySelector(".save-button");
|
||||
ok(
|
||||
content.isVisible(addressSaveButton),
|
||||
"Address save button is rendered"
|
||||
);
|
||||
});
|
||||
|
||||
await fillInShippingAddressForm(
|
||||
frame,
|
||||
PTU.Addresses.TimBL2,
|
||||
addOptions
|
||||
);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkSavedAndCancelButton() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"payment-summary is now visible"
|
||||
);
|
||||
|
||||
let cancelButton = content.document.querySelector("#cancel");
|
||||
ok(
|
||||
content.isVisible(cancelButton),
|
||||
"Payment summary page is shown next"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
info("Closing the payment dialog");
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
cleanupFormAutofillStorage();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_onboarding_wizard_with_requestShipping_turned_off() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
cleanupFormAutofillStorage();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "billing-address-page";
|
||||
// eslint-disable-next-line max-len
|
||||
},
|
||||
"Billing address page is shown first during on-boarding if requestShipping is turned off"
|
||||
);
|
||||
|
||||
info("Checking if the billing address page has been rendered");
|
||||
let addressForm = content.document.querySelector(
|
||||
"#billing-address-page"
|
||||
);
|
||||
let addressSaveButton = addressForm.querySelector(".save-button");
|
||||
ok(
|
||||
content.isVisible(addressSaveButton),
|
||||
"Address save button is rendered"
|
||||
);
|
||||
|
||||
info("Check if the page title is visible on the address page");
|
||||
let addressPageTitle = addressForm.querySelector("h2");
|
||||
ok(
|
||||
content.isVisible(addressPageTitle),
|
||||
"Address page title is visible"
|
||||
);
|
||||
is(
|
||||
addressPageTitle.textContent,
|
||||
"Add Billing Address",
|
||||
"Address page title is correctly shown"
|
||||
);
|
||||
});
|
||||
|
||||
let addOptions = {
|
||||
initialPageId: "basic-card-page",
|
||||
addressPageId: "billing-address-page",
|
||||
};
|
||||
|
||||
await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2, addOptions);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
// eslint-disable-next-line max-len
|
||||
},
|
||||
"Basic card page is shown after the billing address page during onboarding if requestShipping is turned off"
|
||||
);
|
||||
|
||||
let cardSaveButton = content.document.querySelector(
|
||||
"basic-card-form .save-button"
|
||||
);
|
||||
ok(content.isVisible(cardSaveButton), "Basic card page is rendered");
|
||||
|
||||
info(
|
||||
"Check if the correct billing address is selected in the basic card page"
|
||||
);
|
||||
PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
let billingAddressSelect = content.document.querySelector(
|
||||
"#billingAddressGUID"
|
||||
);
|
||||
return (
|
||||
state["basic-card-page"].billingAddressGUID ==
|
||||
billingAddressSelect.value
|
||||
);
|
||||
},
|
||||
"Billing Address is correctly shown"
|
||||
);
|
||||
});
|
||||
|
||||
await fillInCardForm(frame, {
|
||||
["cc-csc"]: "123",
|
||||
...PTU.BasicCards.JohnDoe,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"payment-summary is shown after the basic card page during on boarding"
|
||||
);
|
||||
|
||||
let cancelButton = content.document.querySelector("#cancel");
|
||||
ok(
|
||||
content.isVisible(cancelButton),
|
||||
"Payment summary page is rendered"
|
||||
);
|
||||
});
|
||||
|
||||
info("Closing the payment dialog");
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
|
||||
cleanupFormAutofillStorage();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_on_boarding_wizard_with_requestShipping_turned_off_with_saved_cards() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
cleanupFormAutofillStorage();
|
||||
addBasicCard();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let cancelButton = content.document.querySelector("#cancel");
|
||||
ok(
|
||||
content.isVisible(cancelButton),
|
||||
// eslint-disable-next-line max-len
|
||||
"Payment summary page is shown if requestShipping is turned off and there's a saved card but no saved address"
|
||||
);
|
||||
});
|
||||
|
||||
info("Closing the payment dialog");
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
|
||||
cleanupFormAutofillStorage();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
add_task(
|
||||
async function test_back_button_on_basic_card_page_during_onboarding() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
cleanupFormAutofillStorage();
|
||||
|
||||
info("Opening the payment dialog");
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "billing-address-page";
|
||||
},
|
||||
"Billing address page is shown first if there are no saved addresses " +
|
||||
"and requestShipping is false during on boarding"
|
||||
);
|
||||
info("Checking if the address page has been rendered");
|
||||
let addressSaveButton = content.document.querySelector(
|
||||
"#billing-address-page .save-button"
|
||||
);
|
||||
ok(
|
||||
content.isVisible(addressSaveButton),
|
||||
"Address save button is rendered"
|
||||
);
|
||||
});
|
||||
|
||||
let addOptions = {
|
||||
addLinkSelector: "address-picker.billing-related .add-link",
|
||||
checkboxSelector: "#billing-address-page .persist-checkbox",
|
||||
initialPageId: "basic-card-page",
|
||||
addressPageId: "billing-address-page",
|
||||
expectPersist: true,
|
||||
};
|
||||
|
||||
await fillInBillingAddressForm(frame, PTU.Addresses.TimBL2, addOptions);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
},
|
||||
"Basic card page is shown next"
|
||||
);
|
||||
|
||||
info("Checking if basic card page is rendered");
|
||||
let basicCardBackButton = content.document.querySelector(
|
||||
"basic-card-form .back-button"
|
||||
);
|
||||
ok(
|
||||
content.isVisible(basicCardBackButton),
|
||||
"Back button is visible on the basic card page"
|
||||
);
|
||||
|
||||
info("Partially fill basic card form");
|
||||
let field = content.document.getElementById("cc-number");
|
||||
content.fillField(field, PTU.BasicCards.JohnDoe["cc-number"]);
|
||||
|
||||
info(
|
||||
"Clicking on the back button to edit address saved in the previous step"
|
||||
);
|
||||
basicCardBackButton.click();
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "billing-address-page" &&
|
||||
state["billing-address-page"].guid ==
|
||||
state["basic-card-page"].billingAddressGUID
|
||||
);
|
||||
},
|
||||
"Billing address page is shown again"
|
||||
);
|
||||
|
||||
info("Checking if the address page has been rendered");
|
||||
let addressForm = content.document.querySelector(
|
||||
"#billing-address-page"
|
||||
);
|
||||
let addressSaveButton = addressForm.querySelector(".save-button");
|
||||
ok(
|
||||
content.isVisible(addressSaveButton),
|
||||
"Address save button is rendered"
|
||||
);
|
||||
|
||||
info(
|
||||
"Checking if the address saved in the last step is correctly loaded in the form"
|
||||
);
|
||||
field = addressForm.querySelector("#given-name");
|
||||
is(
|
||||
field.value,
|
||||
PTU.Addresses.TimBL2["given-name"],
|
||||
"Given name field value is correctly loaded"
|
||||
);
|
||||
|
||||
info("Editing the address and saving again");
|
||||
content.fillField(field, "John");
|
||||
addressSaveButton.click();
|
||||
|
||||
info("Checking if the address was correctly edited");
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "basic-card-page" &&
|
||||
// eslint-disable-next-line max-len
|
||||
state.savedAddresses[
|
||||
state["basic-card-page"].billingAddressGUID
|
||||
]["given-name"] == "John"
|
||||
);
|
||||
},
|
||||
"Address was correctly edited and saved"
|
||||
);
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
info(
|
||||
"Checking if the basic card form is now rendered and if the field values from before are preserved"
|
||||
);
|
||||
let basicCardCancelButton = content.document.querySelector(
|
||||
"basic-card-form .cancel-button"
|
||||
);
|
||||
ok(
|
||||
content.isVisible(basicCardCancelButton),
|
||||
"Cancel button is visible on the basic card page"
|
||||
);
|
||||
field = content.document.getElementById("cc-number");
|
||||
is(
|
||||
field.value,
|
||||
PTU.BasicCards.JohnDoe["cc-number"],
|
||||
"Values in the form are preserved"
|
||||
);
|
||||
});
|
||||
|
||||
info("Closing the payment dialog");
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
|
||||
cleanupFormAutofillStorage();
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
@ -1,93 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const methodData = [PTU.MethodData.basicCard];
|
||||
const details = Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
);
|
||||
|
||||
add_task(async function setup_once() {
|
||||
// add an address and card to avoid the FTU sequence
|
||||
await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBL],
|
||||
[PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_openPreferences() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let prefsTabPromise = BrowserTestUtils.waitForNewTab(
|
||||
gBrowser,
|
||||
"about:preferences#privacy-form-autofill"
|
||||
);
|
||||
|
||||
let prefsLoadedPromise = TestUtils.topicObserved("sync-pane-loaded");
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
function verifyPrefsLink({ isMac }) {
|
||||
let manageTextEl = content.document.querySelector(".manage-text");
|
||||
|
||||
let expectedVisibleEl;
|
||||
if (isMac) {
|
||||
expectedVisibleEl = manageTextEl.querySelector(
|
||||
":scope > span[data-os='mac']"
|
||||
);
|
||||
ok(
|
||||
manageTextEl.innerText.includes("Preferences"),
|
||||
"Visible string includes 'Preferences'"
|
||||
);
|
||||
ok(
|
||||
!manageTextEl.innerText.includes("Options"),
|
||||
"Visible string includes 'Options'"
|
||||
);
|
||||
} else {
|
||||
expectedVisibleEl = manageTextEl.querySelector(
|
||||
":scope > span:not([data-os='mac'])"
|
||||
);
|
||||
ok(
|
||||
!manageTextEl.innerText.includes("Preferences"),
|
||||
"Visible string includes 'Preferences'"
|
||||
);
|
||||
ok(
|
||||
manageTextEl.innerText.includes("Options"),
|
||||
"Visible string includes 'Options'"
|
||||
);
|
||||
}
|
||||
|
||||
let prefsLink = expectedVisibleEl.querySelector("a");
|
||||
ok(prefsLink, "Preferences link should exist");
|
||||
prefsLink.scrollIntoView();
|
||||
EventUtils.synthesizeMouseAtCenter(prefsLink, {}, content);
|
||||
},
|
||||
{
|
||||
isMac: AppConstants.platform == "macosx",
|
||||
}
|
||||
);
|
||||
|
||||
let prefsTab = await prefsTabPromise;
|
||||
ok(prefsTab, "Ensure a tab was opened");
|
||||
await prefsLoadedPromise;
|
||||
|
||||
await BrowserTestUtils.removeTab(prefsTab);
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,154 +0,0 @@
|
||||
/* eslint-disable no-shadow */
|
||||
|
||||
"use strict";
|
||||
|
||||
async function setup() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
// add an address and card to avoid the FTU sequence
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBL],
|
||||
[PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
|
||||
info("associating the card with the billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
prefilledGuids.card1GUID,
|
||||
{
|
||||
billingAddressGUID: prefilledGuids.address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return prefilledGuids;
|
||||
}
|
||||
|
||||
/*
|
||||
* Test that the payerRequested* fields are marked as required
|
||||
* on the payer address form but aren't marked as required on
|
||||
* the shipping address form.
|
||||
*/
|
||||
add_task(async function test_add_link() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: {
|
||||
...PTU.Options.requestShipping,
|
||||
...PTU.Options.requestPayerNameEmailAndPhone,
|
||||
},
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await navigateToAddAddressPage(frame, {
|
||||
addLinkSelector: "address-picker.payer-related .add-link",
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "payer-address-page",
|
||||
expectPersist: true,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let { PaymentTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let addressForm = content.document.querySelector("#payer-address-page");
|
||||
let title = addressForm.querySelector("h2");
|
||||
is(title.textContent, "Add Payer Contact", "Page title should be set");
|
||||
|
||||
let saveButton = addressForm.querySelector(".save-button");
|
||||
is(saveButton.textContent, "Next", "Save button has the correct label");
|
||||
|
||||
info("check that payer requested fields are marked as required");
|
||||
for (let selector of [
|
||||
"#given-name",
|
||||
"#family-name",
|
||||
"#email",
|
||||
"#tel",
|
||||
]) {
|
||||
let element = addressForm.querySelector(selector);
|
||||
ok(element.required, selector + " should be required");
|
||||
}
|
||||
|
||||
let backButton = addressForm.querySelector(".back-button");
|
||||
ok(
|
||||
content.isVisible(backButton),
|
||||
"Back button is visible on the payer address page"
|
||||
);
|
||||
backButton.click();
|
||||
|
||||
await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Switched back to payment-summary from payer address form"
|
||||
);
|
||||
});
|
||||
|
||||
await navigateToAddAddressPage(frame, {
|
||||
addLinkSelector: "address-picker.shipping-related .add-link",
|
||||
addressPageId: "shipping-address-page",
|
||||
initialPageId: "payment-summary",
|
||||
expectPersist: true,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let { PaymentTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let addressForm = content.document.querySelector(
|
||||
"#shipping-address-page"
|
||||
);
|
||||
let title = addressForm.querySelector("address-form h2");
|
||||
is(
|
||||
title.textContent,
|
||||
"Add Shipping Address",
|
||||
"Page title should be set"
|
||||
);
|
||||
|
||||
let saveButton = addressForm.querySelector(".save-button");
|
||||
is(saveButton.textContent, "Next", "Save button has the correct label");
|
||||
|
||||
ok(
|
||||
!addressForm.querySelector("#tel").required,
|
||||
"#tel should not be required"
|
||||
);
|
||||
|
||||
let backButton = addressForm.querySelector(".back-button");
|
||||
ok(
|
||||
content.isVisible(backButton),
|
||||
"Back button is visible on the payer address page"
|
||||
);
|
||||
backButton.click();
|
||||
|
||||
await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Switched back to payment-summary from payer address form"
|
||||
);
|
||||
});
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
@ -1,211 +0,0 @@
|
||||
"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 });
|
||||
let card1GUID = await addCardRecord(card);
|
||||
return { address1GUID: billingAddressGUID, card1GUID };
|
||||
}
|
||||
|
||||
add_task(async function test_complete_success() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// 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() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "456",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking pay");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
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() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "789",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking pay");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,303 +0,0 @@
|
||||
/* eslint-disable no-shadow */
|
||||
|
||||
"use strict";
|
||||
|
||||
const methodData = [PTU.MethodData.basicCard];
|
||||
const details = PTU.Details.total60USD;
|
||||
|
||||
add_task(async function test_initial_state() {
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
let address1GUID = await formAutofillStorage.addresses.add(
|
||||
PTU.Addresses.TimBL
|
||||
);
|
||||
await onChanged;
|
||||
|
||||
onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
let card1GUID = await formAutofillStorage.creditCards.add(
|
||||
PTU.BasicCards.JohnDoe
|
||||
);
|
||||
await onChanged;
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkInitialStore({ address1GUID, card1GUID }) {
|
||||
info("checkInitialStore");
|
||||
let contentWin = Cu.waiveXrays(content);
|
||||
let {
|
||||
savedAddresses,
|
||||
savedBasicCards,
|
||||
} = contentWin.document
|
||||
.querySelector("payment-dialog")
|
||||
.requestStore.getState();
|
||||
|
||||
is(
|
||||
Object.keys(savedAddresses).length,
|
||||
1,
|
||||
"Initially one savedAddresses"
|
||||
);
|
||||
is(
|
||||
savedAddresses[address1GUID].name,
|
||||
"Timothy John Berners-Lee",
|
||||
"Check full name"
|
||||
);
|
||||
is(
|
||||
savedAddresses[address1GUID].guid,
|
||||
address1GUID,
|
||||
"Check address guid matches key"
|
||||
);
|
||||
|
||||
is(
|
||||
Object.keys(savedBasicCards).length,
|
||||
1,
|
||||
"Initially one savedBasicCards"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID]["cc-number"],
|
||||
"************1111",
|
||||
"Check cc-number"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].guid,
|
||||
card1GUID,
|
||||
"Check card guid matches key"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].methodName,
|
||||
"basic-card",
|
||||
"Check card has a methodName of basic-card"
|
||||
);
|
||||
},
|
||||
{
|
||||
address1GUID,
|
||||
card1GUID,
|
||||
}
|
||||
);
|
||||
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
info("adding an address");
|
||||
let address2GUID = await formAutofillStorage.addresses.add(
|
||||
PTU.Addresses.TimBL2
|
||||
);
|
||||
await onChanged;
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkAdd({ address1GUID, address2GUID, card1GUID }) {
|
||||
info("checkAdd");
|
||||
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
let {
|
||||
savedAddresses,
|
||||
savedBasicCards,
|
||||
} = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => !!state.savedAddresses[address2GUID]
|
||||
);
|
||||
|
||||
let addressGUIDs = Object.keys(savedAddresses);
|
||||
is(addressGUIDs.length, 2, "Now two savedAddresses");
|
||||
is(addressGUIDs[0], address1GUID, "Check first address GUID");
|
||||
is(
|
||||
savedAddresses[address1GUID].guid,
|
||||
address1GUID,
|
||||
"Check address 1 guid matches key"
|
||||
);
|
||||
is(addressGUIDs[1], address2GUID, "Check second address GUID");
|
||||
is(
|
||||
savedAddresses[address2GUID].guid,
|
||||
address2GUID,
|
||||
"Check address 2 guid matches key"
|
||||
);
|
||||
|
||||
is(
|
||||
Object.keys(savedBasicCards).length,
|
||||
1,
|
||||
"Still one savedBasicCards"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].guid,
|
||||
card1GUID,
|
||||
"Check card guid matches key"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].methodName,
|
||||
"basic-card",
|
||||
"Check card has a methodName of basic-card"
|
||||
);
|
||||
},
|
||||
{
|
||||
address1GUID,
|
||||
address2GUID,
|
||||
card1GUID,
|
||||
}
|
||||
);
|
||||
|
||||
onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "update"
|
||||
);
|
||||
info("updating the credit expiration");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
card1GUID,
|
||||
{
|
||||
"cc-exp-month": 6,
|
||||
"cc-exp-year": 2029,
|
||||
},
|
||||
true
|
||||
);
|
||||
await onChanged;
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkUpdate({ address1GUID, address2GUID, card1GUID }) {
|
||||
info("checkUpdate");
|
||||
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
let {
|
||||
savedAddresses,
|
||||
savedBasicCards,
|
||||
} = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => !!state.savedAddresses[address2GUID]
|
||||
);
|
||||
|
||||
let addressGUIDs = Object.keys(savedAddresses);
|
||||
is(addressGUIDs.length, 2, "Still two savedAddresses");
|
||||
is(addressGUIDs[0], address1GUID, "Check first address GUID");
|
||||
is(
|
||||
savedAddresses[address1GUID].guid,
|
||||
address1GUID,
|
||||
"Check address 1 guid matches key"
|
||||
);
|
||||
is(addressGUIDs[1], address2GUID, "Check second address GUID");
|
||||
is(
|
||||
savedAddresses[address2GUID].guid,
|
||||
address2GUID,
|
||||
"Check address 2 guid matches key"
|
||||
);
|
||||
|
||||
is(
|
||||
Object.keys(savedBasicCards).length,
|
||||
1,
|
||||
"Still one savedBasicCards"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].guid,
|
||||
card1GUID,
|
||||
"Check card guid matches key"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID]["cc-exp-month"],
|
||||
6,
|
||||
"Check expiry month"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID]["cc-exp-year"],
|
||||
2029,
|
||||
"Check expiry year"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].methodName,
|
||||
"basic-card",
|
||||
"Check card has a methodName of basic-card"
|
||||
);
|
||||
},
|
||||
{
|
||||
address1GUID,
|
||||
address2GUID,
|
||||
card1GUID,
|
||||
}
|
||||
);
|
||||
|
||||
onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "remove"
|
||||
);
|
||||
info("removing the first address");
|
||||
formAutofillStorage.addresses.remove(address1GUID);
|
||||
await onChanged;
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkRemove({ address2GUID, card1GUID }) {
|
||||
info("checkRemove");
|
||||
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
let {
|
||||
savedAddresses,
|
||||
savedBasicCards,
|
||||
} = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => !!state.savedAddresses[address2GUID]
|
||||
);
|
||||
|
||||
is(Object.keys(savedAddresses).length, 1, "Now one savedAddresses");
|
||||
is(
|
||||
savedAddresses[address2GUID].name,
|
||||
"Timothy Johann Berners-Lee",
|
||||
"Check full name"
|
||||
);
|
||||
is(
|
||||
savedAddresses[address2GUID].guid,
|
||||
address2GUID,
|
||||
"Check address guid matches key"
|
||||
);
|
||||
|
||||
is(
|
||||
Object.keys(savedBasicCards).length,
|
||||
1,
|
||||
"Still one savedBasicCards"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID]["cc-number"],
|
||||
"************1111",
|
||||
"Check cc-number"
|
||||
);
|
||||
is(
|
||||
savedBasicCards[card1GUID].guid,
|
||||
card1GUID,
|
||||
"Check card guid matches key"
|
||||
);
|
||||
},
|
||||
{
|
||||
address2GUID,
|
||||
card1GUID,
|
||||
}
|
||||
);
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,250 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_serializeRequest_displayItems() {
|
||||
const testTask = ({ methodData, details }) => {
|
||||
let contentWin = Cu.waiveXrays(content);
|
||||
let store = contentWin.document.querySelector("payment-dialog")
|
||||
.requestStore;
|
||||
let state = store && store.getState();
|
||||
ok(state, "got request store state");
|
||||
|
||||
let expected = details;
|
||||
let actual = state.request.paymentDetails;
|
||||
if (expected.displayItems) {
|
||||
is(
|
||||
actual.displayItems.length,
|
||||
expected.displayItems.length,
|
||||
"displayItems have same length"
|
||||
);
|
||||
for (let i = 0; i < actual.displayItems.length; i++) {
|
||||
let item = actual.displayItems[i],
|
||||
expectedItem = expected.displayItems[i];
|
||||
is(item.label, expectedItem.label, "displayItem label matches");
|
||||
is(
|
||||
item.amount.value,
|
||||
expectedItem.amount.value,
|
||||
"displayItem label matches"
|
||||
);
|
||||
is(
|
||||
item.amount.currency,
|
||||
expectedItem.amount.currency,
|
||||
"displayItem label matches"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
is(
|
||||
actual.displayItems,
|
||||
null,
|
||||
"falsey input displayItems is serialized to null"
|
||||
);
|
||||
}
|
||||
};
|
||||
const args = {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.twoDisplayItems,
|
||||
PTU.Details.total32USD
|
||||
),
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_serializeRequest_shippingOptions() {
|
||||
const testTask = ({ methodData, details, options }) => {
|
||||
let contentWin = Cu.waiveXrays(content);
|
||||
let store = contentWin.document.querySelector("payment-dialog")
|
||||
.requestStore;
|
||||
let state = store && store.getState();
|
||||
ok(state, "got request store state");
|
||||
|
||||
// The following test cases are conditionally todo because
|
||||
// the spec currently does not state the shippingOptions
|
||||
// should be null when requestShipping is not set. A future
|
||||
// spec change (bug 1436903 comments 7-12) will fix this.
|
||||
let cond_is = options && options.requestShipping ? is : todo_is;
|
||||
|
||||
let expected = details;
|
||||
let actual = state.request.paymentDetails;
|
||||
if (expected.shippingOptions) {
|
||||
cond_is(
|
||||
actual.shippingOptions.length,
|
||||
expected.shippingOptions.length,
|
||||
"shippingOptions have same length"
|
||||
);
|
||||
for (let i = 0; i < actual.shippingOptions.length; i++) {
|
||||
let item = actual.shippingOptions[i],
|
||||
expectedItem = expected.shippingOptions[i];
|
||||
cond_is(item.label, expectedItem.label, "shippingOption label matches");
|
||||
cond_is(
|
||||
item.amount.value,
|
||||
expectedItem.amount.value,
|
||||
"shippingOption label matches"
|
||||
);
|
||||
cond_is(
|
||||
item.amount.currency,
|
||||
expectedItem.amount.currency,
|
||||
"shippingOption label matches"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
cond_is(
|
||||
actual.shippingOptions,
|
||||
null,
|
||||
"falsey input shippingOptions is serialized to null"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const argsTestCases = [
|
||||
{
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
},
|
||||
{
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
},
|
||||
];
|
||||
for (let args of argsTestCases) {
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_serializeRequest_paymentMethods() {
|
||||
const testTask = ({ methodData, details }) => {
|
||||
let contentWin = Cu.waiveXrays(content);
|
||||
let store = contentWin.document.querySelector("payment-dialog")
|
||||
.requestStore;
|
||||
let state = store && store.getState();
|
||||
ok(state, "got request store state");
|
||||
|
||||
let result = state.request;
|
||||
is(result.paymentMethods.length, 2, "Correct number of payment methods");
|
||||
ok(
|
||||
result.paymentMethods[0].supportedMethods &&
|
||||
result.paymentMethods[1].supportedMethods,
|
||||
"Both payment methods look valid"
|
||||
);
|
||||
|
||||
let cardMethod = result.paymentMethods.find(
|
||||
m => m.supportedMethods == "basic-card"
|
||||
);
|
||||
is(
|
||||
cardMethod.data.supportedNetworks.length,
|
||||
2,
|
||||
"Correct number of supportedNetworks"
|
||||
);
|
||||
ok(
|
||||
cardMethod.data.supportedNetworks.includes("visa") &&
|
||||
cardMethod.data.supportedNetworks.includes("mastercard"),
|
||||
"Got the expected supportedNetworks contents"
|
||||
);
|
||||
};
|
||||
let basicCardMethod = Object.assign({}, PTU.MethodData.basicCard, {
|
||||
data: {
|
||||
supportedNetworks: ["visa", "mastercard"],
|
||||
},
|
||||
});
|
||||
const args = {
|
||||
methodData: [basicCardMethod, PTU.MethodData.bobPay],
|
||||
details: PTU.Details.total60USD,
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_serializeRequest_modifiers() {
|
||||
const testTask = ({ methodData, details }) => {
|
||||
let contentWin = Cu.waiveXrays(content);
|
||||
let store = contentWin.document.querySelector("payment-dialog")
|
||||
.requestStore;
|
||||
let state = store && store.getState();
|
||||
ok(state, "got request store state");
|
||||
|
||||
let expected = details;
|
||||
let actual = state.request.paymentDetails;
|
||||
|
||||
is(
|
||||
actual.modifiers.length,
|
||||
expected.modifiers.length,
|
||||
"modifiers have same length"
|
||||
);
|
||||
for (let i = 0; i < actual.modifiers.length; i++) {
|
||||
let item = actual.modifiers[i],
|
||||
expectedItem = expected.modifiers[i];
|
||||
is(
|
||||
item.supportedMethods,
|
||||
expectedItem.supportedMethods,
|
||||
"modifier supportedMethods matches"
|
||||
);
|
||||
|
||||
is(
|
||||
item.additionalDisplayItems[0].label,
|
||||
expectedItem.additionalDisplayItems[0].label,
|
||||
"additionalDisplayItems label matches"
|
||||
);
|
||||
is(
|
||||
item.additionalDisplayItems[0].amount.value,
|
||||
expectedItem.additionalDisplayItems[0].amount.value,
|
||||
"additionalDisplayItems amount value matches"
|
||||
);
|
||||
is(
|
||||
item.additionalDisplayItems[0].amount.currency,
|
||||
expectedItem.additionalDisplayItems[0].amount.currency,
|
||||
"additionalDisplayItems amount currency matches"
|
||||
);
|
||||
|
||||
is(
|
||||
item.total.label,
|
||||
expectedItem.total.label,
|
||||
"modifier total label matches"
|
||||
);
|
||||
is(
|
||||
item.total.amount.value,
|
||||
expectedItem.total.amount.value,
|
||||
"modifier label matches"
|
||||
);
|
||||
is(
|
||||
item.total.amount.currency,
|
||||
expectedItem.total.amount.currency,
|
||||
"modifier total currency matches"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const args = {
|
||||
methodData: [PTU.MethodData.basicCard, PTU.MethodData.bobPay],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.twoDisplayItems,
|
||||
PTU.Details.bobPayPaymentModifier,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
});
|
@ -1,121 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
add_task(async function setup() {
|
||||
await addSampleAddressesAndBasicCard();
|
||||
});
|
||||
|
||||
add_task(async function test_request_shipping_present() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
for (let [shippingKey, shippingString] of [
|
||||
[null, "Shipping Address"],
|
||||
["shipping", "Shipping Address"],
|
||||
["delivery", "Delivery Address"],
|
||||
["pickup", "Pickup Address"],
|
||||
]) {
|
||||
let options = {
|
||||
requestShipping: true,
|
||||
};
|
||||
if (shippingKey) {
|
||||
options.shippingType = shippingKey;
|
||||
}
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ([aShippingKey, aShippingString]) => {
|
||||
let shippingOptionPicker = content.document.querySelector(
|
||||
"shipping-option-picker"
|
||||
);
|
||||
ok(
|
||||
content.isVisible(shippingOptionPicker),
|
||||
"shipping-option-picker should be visible"
|
||||
);
|
||||
const addressSelector =
|
||||
"address-picker[selected-state-key='selectedShippingAddress']";
|
||||
let shippingAddressPicker = content.document.querySelector(
|
||||
addressSelector
|
||||
);
|
||||
ok(
|
||||
content.isVisible(shippingAddressPicker),
|
||||
"shipping address picker should be visible"
|
||||
);
|
||||
let shippingOption = shippingAddressPicker.querySelector("label");
|
||||
is(
|
||||
shippingOption.textContent,
|
||||
aShippingString,
|
||||
"Label should be match shipping type: " + aShippingKey
|
||||
);
|
||||
},
|
||||
[shippingKey, shippingString]
|
||||
);
|
||||
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_request_shipping_not_present() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let shippingOptionPicker = content.document.querySelector(
|
||||
"shipping-option-picker"
|
||||
);
|
||||
ok(
|
||||
content.isHidden(shippingOptionPicker),
|
||||
"shipping-option-picker should not be visible"
|
||||
);
|
||||
const addressSelector =
|
||||
"address-picker[selected-state-key='selectedShippingAddress']";
|
||||
let shippingAddress = content.document.querySelector(addressSelector);
|
||||
ok(
|
||||
content.isHidden(shippingAddress),
|
||||
"shipping address picker should not be visible"
|
||||
);
|
||||
});
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,167 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test the merchant calling .retry().
|
||||
*/
|
||||
|
||||
async function setup() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
let billingAddressGUID = await addAddressRecord(PTU.Addresses.TimBL);
|
||||
let card = Object.assign({}, PTU.BasicCards.JohnDoe, { billingAddressGUID });
|
||||
let card1GUID = await addCardRecord(card);
|
||||
return { address1GUID: billingAddressGUID, card1GUID };
|
||||
}
|
||||
|
||||
add_task(async function test_retry_with_genericError() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking the button to try pay the 1st time");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
let retryUpdatePromise = spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkDialog() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "processing";
|
||||
},
|
||||
"Wait for completeStatus from pay button click"
|
||||
);
|
||||
|
||||
is(
|
||||
state.request.completeStatus,
|
||||
"processing",
|
||||
"Check completeStatus is processing"
|
||||
);
|
||||
is(
|
||||
state.request.paymentDetails.error,
|
||||
"",
|
||||
"Check error string is empty"
|
||||
);
|
||||
ok(state.changesPrevented, "Changes prevented");
|
||||
|
||||
state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.error,
|
||||
"My generic error",
|
||||
"Check error string in state"
|
||||
);
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(
|
||||
state.page.id,
|
||||
"payment-summary",
|
||||
"Check still on payment-summary"
|
||||
);
|
||||
|
||||
ok(
|
||||
content.document
|
||||
.querySelector("payment-dialog")
|
||||
.innerText.includes("My generic error"),
|
||||
"Check error visibility"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Add a handler to retry the payment above.
|
||||
info("Tell merchant page to retry with an error string");
|
||||
let retryPromise = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
error: "My generic error",
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await retryUpdatePromise;
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// We can only check the retry response after the closing as it only resolves upon complete.
|
||||
let { retryException } = await retryPromise;
|
||||
ok(
|
||||
!retryException,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
|
||||
// Verify response has the expected properties
|
||||
let expectedDetails = Object.assign(
|
||||
{
|
||||
"cc-security-code": "123",
|
||||
},
|
||||
PTU.BasicCards.JohnDoe
|
||||
);
|
||||
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
result.response.details,
|
||||
expectedDetails,
|
||||
"Check response payment details"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,862 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Test the merchant calling .retry() with field-specific errors.
|
||||
*/
|
||||
|
||||
async function setup() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
// add 2 addresses and 2 cards to avoid the FTU sequence and test address errors
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard(
|
||||
[PTU.Addresses.TimBL, PTU.Addresses.TimBL2],
|
||||
[PTU.BasicCards.JaneMasterCard, PTU.BasicCards.JohnDoe]
|
||||
);
|
||||
|
||||
info("associating card1 with a billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
prefilledGuids.card1GUID,
|
||||
{
|
||||
billingAddressGUID: prefilledGuids.address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
info("associating card2 with a billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
prefilledGuids.card2GUID,
|
||||
{
|
||||
billingAddressGUID: prefilledGuids.address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
return prefilledGuids;
|
||||
}
|
||||
|
||||
add_task(async function test_retry_with_shippingAddressErrors() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = 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.twoShippingOptions,
|
||||
PTU.Details.total60USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "DE");
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card2GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking the button to try pay the 1st time");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
let retryUpdatePromise = spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkDialog() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "processing";
|
||||
},
|
||||
"Wait for completeStatus from pay button click"
|
||||
);
|
||||
|
||||
is(
|
||||
state.request.completeStatus,
|
||||
"processing",
|
||||
"Check completeStatus is processing"
|
||||
);
|
||||
is(
|
||||
state.request.paymentDetails.shippingAddressErrors.country,
|
||||
undefined,
|
||||
"Check country error string is empty"
|
||||
);
|
||||
ok(state.changesPrevented, "Changes prevented");
|
||||
|
||||
state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.shippingAddressErrors.country,
|
||||
"Can only ship to USA",
|
||||
"Check country error string in state"
|
||||
);
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(
|
||||
state.page.id,
|
||||
"payment-summary",
|
||||
"Check still on payment-summary"
|
||||
);
|
||||
|
||||
ok(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("Can only ship to USA"),
|
||||
"Check error visibility on summary page"
|
||||
);
|
||||
ok(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Add a handler to retry the payment above.
|
||||
info("Tell merchant page to retry with a country error string");
|
||||
let retryPromise = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
shippingAddress: {
|
||||
country: "Can only ship to USA",
|
||||
},
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await retryUpdatePromise;
|
||||
|
||||
info("Changing to a US address to clear the error");
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "US");
|
||||
|
||||
info("Tell merchant page to retry with a regionCode error string");
|
||||
let retryPromise2 = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
shippingAddress: {
|
||||
regionCode: "Can only ship to California",
|
||||
},
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function checkRegionError() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.shippingAddressErrors.regionCode,
|
||||
"Can only ship to California",
|
||||
"Check regionCode error string in state"
|
||||
);
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(state.page.id, "payment-summary", "Check still on payment-summary");
|
||||
|
||||
ok(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("Can only ship to California"),
|
||||
"Check error visibility on summary page"
|
||||
);
|
||||
ok(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
});
|
||||
|
||||
info(
|
||||
"Changing the shipping state to CA without changing selectedShippingAddress"
|
||||
);
|
||||
await navigateToAddShippingAddressPage(frame, {
|
||||
addLinkSelector:
|
||||
'address-picker[selected-state-key="selectedShippingAddress"] .edit-link',
|
||||
});
|
||||
await fillInShippingAddressForm(frame, { "address-level1": "CA" });
|
||||
await submitAddressForm(frame, null, { isEditing: true });
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// We can only check the retry response after the closing as it only resolves upon complete.
|
||||
let { retryException } = await retryPromise;
|
||||
ok(
|
||||
!retryException,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
let { retryException2 } = await retryPromise2;
|
||||
ok(
|
||||
!retryException2,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
|
||||
// Verify response has the expected properties
|
||||
let expectedDetails = Object.assign(
|
||||
{
|
||||
"cc-security-code": "123",
|
||||
},
|
||||
PTU.BasicCards.JohnDoe
|
||||
);
|
||||
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
result.response.details,
|
||||
expectedDetails,
|
||||
"Check response payment details"
|
||||
);
|
||||
checkPaymentAddressMatchesStorageAddress(
|
||||
result.response.shippingAddress,
|
||||
{ ...PTU.Addresses.TimBL, ...{ "address-level1": "CA" } },
|
||||
"Check response shipping address"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_retry_with_payerErrors() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = await setup();
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
options: PTU.Options.requestPayerNameEmailAndPhone,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card2GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking the button to try pay the 1st time");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
let retryUpdatePromise = spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkDialog() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "processing";
|
||||
},
|
||||
"Wait for completeStatus from pay button click"
|
||||
);
|
||||
|
||||
is(
|
||||
state.request.completeStatus,
|
||||
"processing",
|
||||
"Check completeStatus is processing"
|
||||
);
|
||||
|
||||
is(
|
||||
state.request.paymentDetails.payerErrors.email,
|
||||
undefined,
|
||||
"Check email error isn't present"
|
||||
);
|
||||
ok(state.changesPrevented, "Changes prevented");
|
||||
|
||||
state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.payerErrors.email,
|
||||
"You must use your employee email address",
|
||||
"Check email error string in state"
|
||||
);
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(
|
||||
state.page.id,
|
||||
"payment-summary",
|
||||
"Check still on payment-summary"
|
||||
);
|
||||
|
||||
ok(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("You must use your employee email address"),
|
||||
"Check error visibility on summary page"
|
||||
);
|
||||
ok(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Add a handler to retry the payment above.
|
||||
info("Tell merchant page to retry with a country error string");
|
||||
let retryPromise = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
payer: {
|
||||
email: "You must use your employee email address",
|
||||
},
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await retryUpdatePromise;
|
||||
|
||||
info("Changing to a different email address to clear the error");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectPayerAddressByGuid,
|
||||
prefilledGuids.address1GUID
|
||||
);
|
||||
|
||||
info("Tell merchant page to retry with a phone error string");
|
||||
let retryPromise2 = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
payer: {
|
||||
phone: "Your phone number isn't valid",
|
||||
},
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function checkRegionError() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.payerErrors.phone,
|
||||
"Your phone number isn't valid",
|
||||
"Check regionCode error string in state"
|
||||
);
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(state.page.id, "payment-summary", "Check still on payment-summary");
|
||||
|
||||
ok(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("Your phone number isn't valid"),
|
||||
"Check error visibility on summary page"
|
||||
);
|
||||
ok(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
});
|
||||
|
||||
info(
|
||||
"Changing the payer phone to be valid without changing selectedPayerAddress"
|
||||
);
|
||||
await navigateToAddAddressPage(frame, {
|
||||
addLinkSelector:
|
||||
'address-picker[selected-state-key="selectedPayerAddress"] .edit-link',
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "payer-address-page",
|
||||
});
|
||||
|
||||
let newPhoneNumber = "+16175555555";
|
||||
await fillInPayerAddressForm(frame, { tel: newPhoneNumber });
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "payerdetailchange",
|
||||
},
|
||||
PTU.ContentTasks.promisePaymentResponseEvent
|
||||
);
|
||||
|
||||
await submitAddressForm(frame, null, { isEditing: true });
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "payerdetailchange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// We can only check the retry response after the closing as it only resolves upon complete.
|
||||
let { retryException } = await retryPromise;
|
||||
ok(
|
||||
!retryException,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
let { retryException2 } = await retryPromise2;
|
||||
ok(
|
||||
!retryException2,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
|
||||
// Verify response has the expected properties
|
||||
let expectedDetails = Object.assign(
|
||||
{
|
||||
"cc-security-code": "123",
|
||||
},
|
||||
PTU.BasicCards.JohnDoe
|
||||
);
|
||||
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
result.response.details,
|
||||
expectedDetails,
|
||||
"Check response payment details"
|
||||
);
|
||||
let {
|
||||
"given-name": givenName,
|
||||
"additional-name": additionalName,
|
||||
"family-name": familyName,
|
||||
email,
|
||||
} = PTU.Addresses.TimBL;
|
||||
is(
|
||||
result.response.payerName,
|
||||
`${givenName} ${additionalName} ${familyName}`,
|
||||
"Check payer name"
|
||||
);
|
||||
is(result.response.payerEmail, email, "Check payer email");
|
||||
is(result.response.payerPhone, newPhoneNumber, "Check payer phone");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_retry_with_paymentMethodErrors() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let prefilledGuids = await setup();
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ prefilledGuids: guids }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
},
|
||||
{ prefilledGuids }
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking the button to try pay the 1st time");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
let retryUpdatePromise = spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkDialog() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "processing";
|
||||
},
|
||||
"Wait for completeStatus from pay button click"
|
||||
);
|
||||
|
||||
is(
|
||||
state.request.completeStatus,
|
||||
"processing",
|
||||
"Check completeStatus is processing"
|
||||
);
|
||||
|
||||
is(
|
||||
state.request.paymentDetails.paymentMethodErrors,
|
||||
null,
|
||||
"Check no paymentMethod errors are present"
|
||||
);
|
||||
ok(state.changesPrevented, "Changes prevented");
|
||||
|
||||
state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.paymentMethodErrors.cardSecurityCode,
|
||||
"Your CVV is incorrect",
|
||||
"Check cardSecurityCode error string in state"
|
||||
);
|
||||
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(
|
||||
state.page.id,
|
||||
"payment-summary",
|
||||
"Check still on payment-summary"
|
||||
);
|
||||
|
||||
todo(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("Your CVV is incorrect"),
|
||||
"Bug 1491815: Check error visibility on summary page"
|
||||
);
|
||||
todo(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Bug 1491815: Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
// Add a handler to retry the payment above.
|
||||
info("Tell merchant page to retry with a cardSecurityCode error string");
|
||||
let retryPromise = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
paymentMethod: {
|
||||
cardSecurityCode: "Your CVV is incorrect",
|
||||
},
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await retryUpdatePromise;
|
||||
|
||||
info("Changing to a different card to clear the error");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectPaymentOptionByGuid,
|
||||
prefilledGuids.card1GUID
|
||||
);
|
||||
|
||||
info(
|
||||
"Tell merchant page to retry with a billing postalCode error string"
|
||||
);
|
||||
let retryPromise2 = ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
delayMs: 1000,
|
||||
validationErrors: {
|
||||
paymentMethod: {
|
||||
billingAddress: {
|
||||
postalCode: "Your postal code isn't valid",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PTU.ContentTasks.addRetryHandler
|
||||
);
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkPostalCodeError() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let state = await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
({ request }) => {
|
||||
return request.completeStatus === "";
|
||||
},
|
||||
"Wait for completeStatus from DOM update"
|
||||
);
|
||||
|
||||
is(state.request.completeStatus, "", "Check completeStatus");
|
||||
is(
|
||||
state.request.paymentDetails.paymentMethodErrors.billingAddress
|
||||
.postalCode,
|
||||
"Your postal code isn't valid",
|
||||
"Check postalCode error string in state"
|
||||
);
|
||||
ok(!state.changesPrevented, "Changes no longer prevented");
|
||||
is(
|
||||
state.page.id,
|
||||
"payment-summary",
|
||||
"Check still on payment-summary"
|
||||
);
|
||||
|
||||
todo(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("Your postal code isn't valid"),
|
||||
"Bug 1491815: Check error visibility on summary page"
|
||||
);
|
||||
todo(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Bug 1491815: Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
info(
|
||||
"Changing the billingAddress postalCode to be valid without changing selectedPaymentCard"
|
||||
);
|
||||
|
||||
await navigateToAddCardPage(frame, {
|
||||
addLinkSelector: "payment-method-picker .edit-link",
|
||||
});
|
||||
|
||||
await navigateToAddAddressPage(frame, {
|
||||
addLinkSelector: ".billingAddressRow .edit-link",
|
||||
initialPageId: "basic-card-page",
|
||||
addressPageId: "billing-address-page",
|
||||
});
|
||||
|
||||
let newPostalCode = "90210";
|
||||
await fillInBillingAddressForm(frame, { "postal-code": newPostalCode });
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "paymentmethodchange",
|
||||
},
|
||||
PTU.ContentTasks.promisePaymentResponseEvent
|
||||
);
|
||||
|
||||
await submitAddressForm(frame, null, {
|
||||
isEditing: true,
|
||||
nextPageId: "basic-card-page",
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function checkErrorsCleared() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.request.paymentDetails.paymentMethodErrors == null;
|
||||
},
|
||||
"Check no paymentMethod errors are present"
|
||||
);
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function checkErrorsCleared() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.request.paymentDetails.paymentMethodErrors == null;
|
||||
},
|
||||
"Check no card errors are present after save"
|
||||
);
|
||||
});
|
||||
|
||||
// TODO: Add an `await` here after bug 1477113.
|
||||
ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "paymentmethodchange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// We can only check the retry response after the closing as it only resolves upon complete.
|
||||
let { retryException } = await retryPromise;
|
||||
ok(
|
||||
!retryException,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
let { retryException2 } = await retryPromise2;
|
||||
ok(
|
||||
!retryException2,
|
||||
"Expect no exception to be thrown when calling retry()"
|
||||
);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
|
||||
// Verify response has the expected properties
|
||||
let expectedDetails = Object.assign(
|
||||
{
|
||||
"cc-security-code": "123",
|
||||
},
|
||||
PTU.BasicCards.JaneMasterCard
|
||||
);
|
||||
|
||||
let expectedBillingAddress = Object.assign({}, PTU.Addresses.TimBL, {
|
||||
"postal-code": newPostalCode,
|
||||
});
|
||||
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
result.response.details,
|
||||
expectedDetails,
|
||||
"Check response payment details"
|
||||
);
|
||||
checkPaymentAddressMatchesStorageAddress(
|
||||
result.response.details.billingAddress,
|
||||
expectedBillingAddress,
|
||||
"Check response billing address"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,430 +0,0 @@
|
||||
/* eslint-disable no-shadow */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(addSampleAddressesAndBasicCard);
|
||||
|
||||
add_task(async function test_show_error_on_addresschange() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
info("setting up the event handler for shippingoptionchange");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingoptionchange",
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.genericShippingError,
|
||||
PTU.Details.noShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectShippingOptionById,
|
||||
"1"
|
||||
);
|
||||
|
||||
info("awaiting the shippingoptionchange event");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingoptionchange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
expectedText => {
|
||||
let errorText = content.document.querySelector("header .page-error");
|
||||
is(
|
||||
errorText.textContent,
|
||||
expectedText,
|
||||
"Error text should be on dialog"
|
||||
);
|
||||
ok(content.isVisible(errorText), "Error text should be visible");
|
||||
},
|
||||
PTU.Details.genericShippingError.error
|
||||
);
|
||||
|
||||
info("setting up the event handler for shippingaddresschange");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.noError,
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "DE");
|
||||
|
||||
info("awaiting the shippingaddresschange event");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, () => {
|
||||
let errorText = content.document.querySelector("header .page-error");
|
||||
is(errorText.textContent, "", "Error text should not be on dialog");
|
||||
ok(content.isHidden(errorText), "Error text should not be visible");
|
||||
});
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_show_field_specific_error_on_addresschange() {
|
||||
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.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
info("setting up the event handler for shippingaddresschange");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.fieldSpecificErrors,
|
||||
PTU.Details.noShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
|
||||
spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectShippingAddressByCountry,
|
||||
"DE"
|
||||
);
|
||||
|
||||
info("awaiting the shippingaddresschange event");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return Object.keys(
|
||||
state.request.paymentDetails.shippingAddressErrors
|
||||
).length;
|
||||
},
|
||||
"Check that there are shippingAddressErrors"
|
||||
);
|
||||
|
||||
is(
|
||||
content.document.querySelector("header .page-error").textContent,
|
||||
PTU.Details.fieldSpecificErrors.error,
|
||||
"Error text should be present on dialog"
|
||||
);
|
||||
|
||||
info("click the Edit link");
|
||||
content.document
|
||||
.querySelector("address-picker.shipping-related .edit-link")
|
||||
.click();
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "shipping-address-page" &&
|
||||
state["shipping-address-page"].guid
|
||||
);
|
||||
},
|
||||
"Check edit page state"
|
||||
);
|
||||
|
||||
// check errors and make corrections
|
||||
let addressForm = content.document.querySelector(
|
||||
"#shipping-address-page"
|
||||
);
|
||||
let { shippingAddressErrors } = PTU.Details.fieldSpecificErrors;
|
||||
is(
|
||||
addressForm.querySelectorAll(".error-text:not(:empty)").length,
|
||||
Object.keys(shippingAddressErrors).length - 1,
|
||||
"Each error should be presented, but only one of region and regionCode are displayed"
|
||||
);
|
||||
let errorFieldMap = Cu.waiveXrays(addressForm)._errorFieldMap;
|
||||
for (let [errorName, errorValue] of Object.entries(
|
||||
shippingAddressErrors
|
||||
)) {
|
||||
if (errorName == "region" || errorName == "regionCode") {
|
||||
errorValue = shippingAddressErrors.regionCode;
|
||||
}
|
||||
let fieldSelector = errorFieldMap[errorName];
|
||||
let containerSelector = fieldSelector + "-container";
|
||||
let container = addressForm.querySelector(containerSelector);
|
||||
try {
|
||||
is(
|
||||
container.querySelector(".error-text").textContent,
|
||||
errorValue,
|
||||
"Field specific error should be associated with " + errorName
|
||||
);
|
||||
} catch (ex) {
|
||||
ok(
|
||||
false,
|
||||
`no container for ${errorName}. selector= ${containerSelector}`
|
||||
);
|
||||
}
|
||||
try {
|
||||
let field = addressForm.querySelector(fieldSelector);
|
||||
let oldValue = field.value;
|
||||
if (field.localName == "select") {
|
||||
// Flip between empty and the selected entry so country fields won't change.
|
||||
content.fillField(field, "");
|
||||
content.fillField(field, oldValue);
|
||||
} else {
|
||||
content.fillField(
|
||||
field,
|
||||
field.value
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("")
|
||||
);
|
||||
}
|
||||
} catch (ex) {
|
||||
ok(
|
||||
false,
|
||||
`no field found for ${errorName}. selector= ${fieldSelector}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
info(
|
||||
"setting up the event handler for a 2nd shippingaddresschange with a different error"
|
||||
);
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: Object.assign(
|
||||
{},
|
||||
{
|
||||
shippingAddressErrors: {
|
||||
phone: "Invalid phone number",
|
||||
},
|
||||
},
|
||||
PTU.Details.noShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function checkForNewErrors() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "payment-summary" &&
|
||||
state.request.paymentDetails.shippingAddressErrors.phone ==
|
||||
"Invalid phone number"
|
||||
);
|
||||
},
|
||||
"Check the new error is in state"
|
||||
);
|
||||
|
||||
ok(
|
||||
content.document
|
||||
.querySelector("#payment-summary")
|
||||
.innerText.includes("Invalid phone number"),
|
||||
"Check error visibility on summary page"
|
||||
);
|
||||
ok(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"Pay button should be disabled until the field error is addressed"
|
||||
);
|
||||
});
|
||||
|
||||
await navigateToAddShippingAddressPage(frame, {
|
||||
addLinkSelector:
|
||||
'address-picker[selected-state-key="selectedShippingAddress"] .edit-link',
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async function checkForNewErrorOnEdit() {
|
||||
let addressForm = content.document.querySelector(
|
||||
"#shipping-address-page"
|
||||
);
|
||||
is(
|
||||
addressForm.querySelectorAll(".error-text:not(:empty)").length,
|
||||
1,
|
||||
"Check one error shown"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await fillInShippingAddressForm(frame, {
|
||||
tel: PTU.Addresses.TimBL2.tel,
|
||||
});
|
||||
|
||||
info("setup updateWith to clear errors");
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingaddresschange",
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
},
|
||||
PTU.ContentTasks.updateWith
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function fixLastError() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Check we're back on summary view"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return !Object.keys(
|
||||
state.request.paymentDetails.shippingAddressErrors
|
||||
).length;
|
||||
},
|
||||
"Check that there are no more shippingAddressErrors"
|
||||
);
|
||||
|
||||
is(
|
||||
content.document.querySelector("header .page-error").textContent,
|
||||
"",
|
||||
"Error text should not be present on dialog"
|
||||
);
|
||||
|
||||
info("click the Edit link again");
|
||||
content.document.querySelector("address-picker .edit-link").click();
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return (
|
||||
state.page.id == "shipping-address-page" &&
|
||||
state["shipping-address-page"].guid
|
||||
);
|
||||
},
|
||||
"Check edit page state"
|
||||
);
|
||||
|
||||
let addressForm = content.document.querySelector(
|
||||
"#shipping-address-page"
|
||||
);
|
||||
// check no errors present
|
||||
let errorTextSpans = addressForm.querySelectorAll(
|
||||
".error-text:not(:empty)"
|
||||
);
|
||||
for (let errorTextSpan of errorTextSpans) {
|
||||
is(
|
||||
errorTextSpan.textContent,
|
||||
"",
|
||||
"No errors should be present on the field"
|
||||
);
|
||||
}
|
||||
|
||||
info("click the Back button");
|
||||
addressForm.querySelector(".back-button").click();
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "payment-summary";
|
||||
},
|
||||
"Check we're back on summary view"
|
||||
);
|
||||
});
|
||||
|
||||
info("clicking cancel");
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,396 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const methodData = [PTU.MethodData.basicCard];
|
||||
const details = Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
);
|
||||
|
||||
add_task(async function test_show_abort_dialog() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
// abort the payment request
|
||||
ContentTask.spawn(browser, null, async () => content.rq.abort());
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_show_manualAbort_dialog() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_show_completePayment() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
let { address1GUID, card1GUID } = await addSampleAddressesAndBasicCard();
|
||||
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "update"
|
||||
);
|
||||
info("associating the card with the billing address");
|
||||
await formAutofillStorage.creditCards.update(
|
||||
card1GUID,
|
||||
{
|
||||
billingAddressGUID: address1GUID,
|
||||
},
|
||||
true
|
||||
);
|
||||
await onChanged;
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
info("select the shipping address");
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "US");
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async ({ card1GUID: cardGuid }) => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
cardGuid
|
||||
);
|
||||
},
|
||||
{ card1GUID }
|
||||
);
|
||||
|
||||
info("entering CSC");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "999",
|
||||
}
|
||||
);
|
||||
info("clicking pay");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
|
||||
let { shippingAddress } = result.response;
|
||||
checkPaymentAddressMatchesStorageAddress(
|
||||
shippingAddress,
|
||||
PTU.Addresses.TimBL,
|
||||
"Shipping"
|
||||
);
|
||||
|
||||
is(result.response.methodName, "basic-card", "Check methodName");
|
||||
let { methodDetails } = result;
|
||||
checkPaymentMethodDetailsMatchesCard(
|
||||
methodDetails,
|
||||
PTU.BasicCards.JohnDoe,
|
||||
"Payment method"
|
||||
);
|
||||
is(methodDetails.cardSecurityCode, "999", "Check cardSecurityCode");
|
||||
is(
|
||||
typeof methodDetails.methodName,
|
||||
"undefined",
|
||||
"Check methodName wasn't included"
|
||||
);
|
||||
|
||||
checkPaymentAddressMatchesStorageAddress(
|
||||
methodDetails.billingAddress,
|
||||
PTU.Addresses.TimBL,
|
||||
"Billing address"
|
||||
);
|
||||
|
||||
is(result.response.shippingOption, "2", "Check shipping option");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_show_completePayment2() {
|
||||
if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) {
|
||||
todo(false, "Cannot test OS key store login on official builds.");
|
||||
return;
|
||||
}
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
options: PTU.Options.requestShippingOption,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingoptionchange",
|
||||
},
|
||||
PTU.ContentTasks.promisePaymentRequestEvent
|
||||
);
|
||||
|
||||
info(
|
||||
"changing shipping option to '1' from default selected option of '2'"
|
||||
);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectShippingOptionById,
|
||||
"1"
|
||||
);
|
||||
|
||||
await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
eventName: "shippingoptionchange",
|
||||
},
|
||||
PTU.ContentTasks.awaitPaymentEventPromise
|
||||
);
|
||||
info("got shippingoptionchange event");
|
||||
|
||||
info("select the shipping address");
|
||||
await selectPaymentDialogShippingAddressByCountry(frame, "US");
|
||||
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox.options[0].value
|
||||
);
|
||||
});
|
||||
|
||||
info("entering CSC");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "123",
|
||||
}
|
||||
);
|
||||
|
||||
info("clicking pay");
|
||||
await loginAndCompletePayment(frame);
|
||||
|
||||
// Add a handler to complete the payment above.
|
||||
info("acknowledging the completion from the merchant page");
|
||||
let result = await ContentTask.spawn(
|
||||
browser,
|
||||
{},
|
||||
PTU.ContentTasks.addCompletionHandler
|
||||
);
|
||||
|
||||
is(result.response.shippingOption, "1", "Check shipping option");
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_localized() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function check_l10n() {
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
let telephoneLabel = content.document.querySelector(
|
||||
"#tel-container > .label-text"
|
||||
);
|
||||
return telephoneLabel && telephoneLabel.textContent.includes("Phone");
|
||||
}, "Check that the telephone number label is localized");
|
||||
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
let ccNumberField = content.document.querySelector("#cc-number");
|
||||
if (!ccNumberField) {
|
||||
return false;
|
||||
}
|
||||
let ccNumberLabel = ccNumberField.parentElement.querySelector(
|
||||
".label-text"
|
||||
);
|
||||
return ccNumberLabel.textContent.includes("Number");
|
||||
}, "Check that the cc-number label is localized");
|
||||
|
||||
const L10N_ATTRIBUTE_SELECTOR =
|
||||
"[data-localization], [data-localization-region]";
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
return (
|
||||
content.document.querySelectorAll(L10N_ATTRIBUTE_SELECTOR)
|
||||
.length === 0
|
||||
);
|
||||
}, "Check that there are no unlocalized strings");
|
||||
});
|
||||
|
||||
// abort the payment request
|
||||
ContentTask.spawn(browser, null, async () => content.rq.abort());
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_supportedNetworks() {
|
||||
await setupFormAutofillStorage();
|
||||
await cleanupFormAutofillStorage();
|
||||
|
||||
let address1GUID = await addAddressRecord(PTU.Addresses.TimBL);
|
||||
let visaCardGUID = await addCardRecord(
|
||||
Object.assign({}, PTU.BasicCards.JohnDoe, {
|
||||
billingAddressGUID: address1GUID,
|
||||
})
|
||||
);
|
||||
let masterCardGUID = await addCardRecord(
|
||||
Object.assign({}, PTU.BasicCards.JaneMasterCard, {
|
||||
billingAddressGUID: address1GUID,
|
||||
})
|
||||
);
|
||||
|
||||
let cardMethod = {
|
||||
supportedMethods: "basic-card",
|
||||
data: {
|
||||
supportedNetworks: ["visa"],
|
||||
},
|
||||
};
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData: [cardMethod],
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
info("entering CSC");
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.setSecurityCode,
|
||||
{
|
||||
securityCode: "789",
|
||||
}
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, () => {
|
||||
let acceptedCards = content.document.querySelector("accepted-cards");
|
||||
ok(
|
||||
acceptedCards && !content.isHidden(acceptedCards),
|
||||
"accepted-cards element is present and visible"
|
||||
);
|
||||
is(
|
||||
Cu.waiveXrays(acceptedCards).acceptedItems.length,
|
||||
1,
|
||||
"accepted-cards element has 1 item"
|
||||
);
|
||||
});
|
||||
|
||||
info("select the mastercard using guid: " + masterCardGUID);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectPaymentOptionByGuid,
|
||||
masterCardGUID
|
||||
);
|
||||
|
||||
info("spawn task to check pay button with mastercard selected");
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
ok(
|
||||
content.document.getElementById("pay").disabled,
|
||||
"pay button should be disabled"
|
||||
);
|
||||
});
|
||||
|
||||
info("select the visa using guid: " + visaCardGUID);
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectPaymentOptionByGuid,
|
||||
visaCardGUID
|
||||
);
|
||||
|
||||
info("spawn task to check pay button");
|
||||
await spawnPaymentDialogTask(frame, async () => {
|
||||
ok(
|
||||
!content.document.getElementById("pay").disabled,
|
||||
"pay button should not be disabled"
|
||||
);
|
||||
});
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
@ -1,300 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
const methodData = [PTU.MethodData.basicCard];
|
||||
const details = Object.assign(
|
||||
{},
|
||||
PTU.Details.twoShippingOptions,
|
||||
PTU.Details.total2USD
|
||||
);
|
||||
|
||||
async function checkTabModal(browser, win, msg) {
|
||||
info(`checkTabModal: ${msg}`);
|
||||
let doc = browser.ownerDocument;
|
||||
await TestUtils.waitForCondition(() => {
|
||||
return !doc.querySelector(".paymentDialogContainer").hidden;
|
||||
}, "Waiting for container to be visible after the dialog's ready");
|
||||
is(
|
||||
doc.querySelectorAll(".paymentDialogContainer").length,
|
||||
1,
|
||||
"Only 1 paymentDialogContainer"
|
||||
);
|
||||
ok(!EventUtils.isHidden(win.frameElement), "Frame should be visible");
|
||||
|
||||
let { bottom: toolboxBottom } = doc
|
||||
.getElementById("navigator-toolbox")
|
||||
.getBoundingClientRect();
|
||||
|
||||
let { x, y } = win.frameElement.getBoundingClientRect();
|
||||
ok(y > 0, "Frame should have y > 0");
|
||||
// Inset by 10px since the corner point doesn't return the frame due to the
|
||||
// border-radius.
|
||||
is(
|
||||
doc.elementFromPoint(x + 10, y + 10),
|
||||
win.frameElement,
|
||||
"Check .paymentDialogContainerFrame is visible"
|
||||
);
|
||||
|
||||
info("Click to the left of the dialog over the content area");
|
||||
isnot(
|
||||
doc.elementFromPoint(x - 10, y + 50),
|
||||
browser,
|
||||
"Check clicks on the merchant content area don't go to the browser"
|
||||
);
|
||||
is(
|
||||
doc.elementFromPoint(x - 10, y + 50),
|
||||
doc.querySelector(".paymentDialogBackground"),
|
||||
"Check clicks on the merchant content area go to the payment dialog background"
|
||||
);
|
||||
|
||||
ok(
|
||||
y < toolboxBottom - 2,
|
||||
"Dialog should overlap the toolbox by at least 2px"
|
||||
);
|
||||
|
||||
ok(
|
||||
browser.hasAttribute("tabmodalPromptShowing"),
|
||||
"Check browser has @tabmodalPromptShowing"
|
||||
);
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
}
|
||||
|
||||
add_task(async function test_tab_modal() {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async browser => {
|
||||
let { win, frame } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
let { x, y } = await checkTabModal(browser, win, "initial dialog");
|
||||
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
async newBrowser => {
|
||||
let { x: x2, y: y2 } = win.frameElement.getBoundingClientRect();
|
||||
is(x2, x, "Check x-coordinate is the same");
|
||||
is(y2, y, "Check y-coordinate is the same");
|
||||
isnot(
|
||||
document.elementFromPoint(x + 10, y + 10),
|
||||
win.frameElement,
|
||||
"Check .paymentDialogContainerFrame is hidden"
|
||||
);
|
||||
ok(
|
||||
!newBrowser.hasAttribute("tabmodalPromptShowing"),
|
||||
"Check second browser doesn't have @tabmodalPromptShowing"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
let { x: x3, y: y3 } = await checkTabModal(
|
||||
browser,
|
||||
win,
|
||||
"after tab switch back"
|
||||
);
|
||||
is(x3, x, "Check x-coordinate is the same again");
|
||||
is(y3, y, "Check y-coordinate is the same again");
|
||||
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => !browser.hasAttribute("tabmodalPromptShowing"),
|
||||
"Check @tabmodalPromptShowing was removed"
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_detachToNewWindow() {
|
||||
let tab = await BrowserTestUtils.openNewForegroundTab({
|
||||
gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
});
|
||||
let browser = tab.linkedBrowser;
|
||||
|
||||
let { frame, requestId } = await setupPaymentDialog(browser, {
|
||||
methodData,
|
||||
details,
|
||||
merchantTaskFn: PTU.ContentTasks.createAndShowRequest,
|
||||
});
|
||||
|
||||
is(
|
||||
Object.values(frame.paymentDialogWrapper.temporaryStore.addresses.getAll())
|
||||
.length,
|
||||
0,
|
||||
"Check initial temp. address store"
|
||||
);
|
||||
is(
|
||||
Object.values(
|
||||
frame.paymentDialogWrapper.temporaryStore.creditCards.getAll()
|
||||
).length,
|
||||
0,
|
||||
"Check initial temp. card store"
|
||||
);
|
||||
|
||||
info("Create some temp. records so we can later check if they are preserved");
|
||||
let address1 = { ...PTU.Addresses.Temp };
|
||||
let card1 = { ...PTU.BasicCards.JaneMasterCard, ...{ "cc-csc": "123" } };
|
||||
|
||||
await fillInBillingAddressForm(frame, address1, {
|
||||
setPersistCheckedValue: false,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
await spawnPaymentDialogTask(frame, async function waitForPageChange() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
},
|
||||
"Wait for basic-card-page"
|
||||
);
|
||||
});
|
||||
|
||||
await fillInCardForm(frame, card1, {
|
||||
checkboxSelector: "basic-card-form .persist-checkbox",
|
||||
setPersistCheckedValue: false,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.clickPrimaryButton
|
||||
);
|
||||
|
||||
let { temporaryStore } = frame.paymentDialogWrapper;
|
||||
TestUtils.waitForCondition(() => {
|
||||
return Object.values(temporaryStore.addresses.getAll()).length == 1;
|
||||
}, "Check address store");
|
||||
TestUtils.waitForCondition(() => {
|
||||
return Object.values(temporaryStore.creditCards.getAll()).length == 1;
|
||||
}, "Check card store");
|
||||
|
||||
let windowLoadedPromise = BrowserTestUtils.waitForNewWindow();
|
||||
let newWin = gBrowser.replaceTabWithWindow(tab);
|
||||
await windowLoadedPromise;
|
||||
|
||||
info("tab was detached");
|
||||
let newBrowser = newWin.gBrowser.selectedBrowser;
|
||||
ok(newBrowser, "Found new <browser>");
|
||||
|
||||
let widget = await TestUtils.waitForCondition(async () =>
|
||||
getPaymentWidget(requestId)
|
||||
);
|
||||
await checkTabModal(newBrowser, widget, "after detach");
|
||||
|
||||
let state = await spawnPaymentDialogTask(
|
||||
widget.frameElement,
|
||||
async function checkAfterDetach() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
return PTU.DialogContentUtils.getCurrentState(content);
|
||||
}
|
||||
);
|
||||
|
||||
is(
|
||||
Object.values(state.tempAddresses).length,
|
||||
1,
|
||||
"Check still 1 temp. address in state"
|
||||
);
|
||||
is(
|
||||
Object.values(state.tempBasicCards).length,
|
||||
1,
|
||||
"Check still 1 temp. basic card in state"
|
||||
);
|
||||
|
||||
temporaryStore = widget.frameElement.paymentDialogWrapper.temporaryStore;
|
||||
is(
|
||||
Object.values(temporaryStore.addresses.getAll()).length,
|
||||
1,
|
||||
"Check address store in wrapper"
|
||||
);
|
||||
is(
|
||||
Object.values(temporaryStore.creditCards.getAll()).length,
|
||||
1,
|
||||
"Check card store in wrapper"
|
||||
);
|
||||
|
||||
info(
|
||||
"Check that the message manager and formautofill-storage-changed observer are connected"
|
||||
);
|
||||
is(Object.values(state.savedAddresses).length, 0, "Check 0 saved addresses");
|
||||
await addAddressRecord(PTU.Addresses.TimBL2);
|
||||
await spawnPaymentDialogTask(
|
||||
widget.frameElement,
|
||||
async function waitForSavedAddress() {
|
||||
let { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
await PTU.DialogContentUtils.waitForState(
|
||||
content,
|
||||
function checkSavedAddresses(s) {
|
||||
return Object.values(s.savedAddresses).length == 1;
|
||||
},
|
||||
"Check 1 saved address in state"
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
info(
|
||||
"re-attach the tab back in the original window to test the event listeners were added"
|
||||
);
|
||||
|
||||
let tab3 = gBrowser.adoptTab(newWin.gBrowser.selectedTab, 1, true);
|
||||
widget = await TestUtils.waitForCondition(async () =>
|
||||
getPaymentWidget(requestId)
|
||||
);
|
||||
is(
|
||||
widget.frameElement.ownerGlobal,
|
||||
window,
|
||||
"Check widget is back in first window"
|
||||
);
|
||||
await checkTabModal(tab3.linkedBrowser, widget, "after re-attaching");
|
||||
|
||||
temporaryStore = widget.frameElement.paymentDialogWrapper.temporaryStore;
|
||||
is(
|
||||
Object.values(temporaryStore.addresses.getAll()).length,
|
||||
1,
|
||||
"Check temp addresses in wrapper"
|
||||
);
|
||||
is(
|
||||
Object.values(temporaryStore.creditCards.getAll()).length,
|
||||
1,
|
||||
"Check temp cards in wrapper"
|
||||
);
|
||||
|
||||
spawnPaymentDialogTask(
|
||||
widget.frameElement,
|
||||
PTU.DialogContentTasks.manuallyClickCancel
|
||||
);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => widget.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
await BrowserTestUtils.removeTab(tab3);
|
||||
});
|
@ -1,94 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
add_task(async function test_total() {
|
||||
const testTask = ({ methodData, details }) => {
|
||||
is(
|
||||
content.document.querySelector("#total > currency-amount").textContent,
|
||||
"$60.00 USD",
|
||||
"Check total currency amount"
|
||||
);
|
||||
};
|
||||
const args = {
|
||||
methodData: [PTU.MethodData.basicCard],
|
||||
details: PTU.Details.total60USD,
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_modifier_with_no_method_selected() {
|
||||
const testTask = async ({ methodData, details }) => {
|
||||
// There are no payment methods installed/setup so we expect the original (unmodified) total.
|
||||
is(
|
||||
content.document.querySelector("#total > currency-amount").textContent,
|
||||
"$2.00 USD",
|
||||
"Check unmodified total currency amount"
|
||||
);
|
||||
};
|
||||
const args = {
|
||||
methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.bobPayPaymentModifier,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
});
|
||||
|
||||
add_task(async function test_modifier_with_no_method_selected() {
|
||||
info("adding a basic-card");
|
||||
let prefilledGuids = await addSampleAddressesAndBasicCard();
|
||||
|
||||
const testTask = async ({ methodData, details, prefilledGuids: guids }) => {
|
||||
is(
|
||||
content.document.querySelector("#total > currency-amount").textContent,
|
||||
"$2.00 USD",
|
||||
"Check total currency amount before selecting the credit card"
|
||||
);
|
||||
|
||||
// Select the (only) payment method.
|
||||
let paymentMethodPicker = content.document.querySelector(
|
||||
"payment-method-picker"
|
||||
);
|
||||
content.fillField(
|
||||
Cu.waiveXrays(paymentMethodPicker).dropdown.popupBox,
|
||||
guids.card1GUID
|
||||
);
|
||||
|
||||
await ContentTaskUtils.waitForCondition(() => {
|
||||
let currencyAmount = content.document.querySelector(
|
||||
"#total > currency-amount"
|
||||
);
|
||||
return currencyAmount.textContent == "$2.50 USD";
|
||||
}, "Wait for modified total to update");
|
||||
|
||||
is(
|
||||
content.document.querySelector("#total > currency-amount").textContent,
|
||||
"$2.50 USD",
|
||||
"Check modified total currency amount"
|
||||
);
|
||||
};
|
||||
const args = {
|
||||
methodData: [PTU.MethodData.bobPay, PTU.MethodData.basicCard],
|
||||
details: Object.assign(
|
||||
{},
|
||||
PTU.Details.bobPayPaymentModifier,
|
||||
PTU.Details.total2USD
|
||||
),
|
||||
prefilledGuids,
|
||||
};
|
||||
await spawnInDialogForMerchantTask(
|
||||
PTU.ContentTasks.createAndShowRequest,
|
||||
testTask,
|
||||
args
|
||||
);
|
||||
await cleanupFormAutofillStorage();
|
||||
});
|
@ -1,969 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/* eslint
|
||||
"no-unused-vars": ["error", {
|
||||
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 SAVE_CREDITCARD_DEFAULT_PREF = "dom.payments.defaults.saveCreditCard";
|
||||
const SAVE_ADDRESS_DEFAULT_PREF = "dom.payments.defaults.saveAddress";
|
||||
|
||||
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(Ci.nsIPaymentUIService).wrappedJSObject;
|
||||
const { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
const { formAutofillStorage } = ChromeUtils.import(
|
||||
"resource://formautofill/FormAutofillStorage.jsm"
|
||||
);
|
||||
const { OSKeyStoreTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/OSKeyStoreTestUtils.jsm"
|
||||
);
|
||||
const { PaymentTestUtils: PTU } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
var { BrowserWindowTracker } = ChromeUtils.import(
|
||||
"resource:///modules/BrowserWindowTracker.jsm"
|
||||
);
|
||||
var { CreditCard } = ChromeUtils.import(
|
||||
"resource://gre/modules/CreditCard.jsm"
|
||||
);
|
||||
|
||||
function getPaymentRequests() {
|
||||
return Array.from(paymentSrv.enumerate());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the container (e.g. dialog or overlay) that the payment request contents are shown in.
|
||||
* This abstracts away the details of the widget used so that this can more easily transition to
|
||||
* another kind of dialog/overlay.
|
||||
* @param {string} requestId
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function getPaymentWidget(requestId) {
|
||||
return BrowserTestUtils.waitForCondition(() => {
|
||||
let { dialogContainer } = paymentUISrv.findDialog(requestId);
|
||||
if (!dialogContainer) {
|
||||
return false;
|
||||
}
|
||||
let paymentFrame = dialogContainer.querySelector(
|
||||
".paymentDialogContainerFrame"
|
||||
);
|
||||
if (!paymentFrame) {
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
get closed() {
|
||||
return !paymentFrame.isConnected;
|
||||
},
|
||||
frameElement: paymentFrame,
|
||||
};
|
||||
}, "payment dialog should be opened");
|
||||
}
|
||||
|
||||
async function getPaymentFrame(widget) {
|
||||
return widget.frameElement;
|
||||
}
|
||||
|
||||
function waitForMessageFromWidget(messageType, widget = null) {
|
||||
info("waitForMessageFromWidget: " + messageType);
|
||||
return new Promise(resolve => {
|
||||
Services.mm.addMessageListener(
|
||||
"paymentContentToChrome",
|
||||
function onMessage({ data, target }) {
|
||||
if (data.messageType != messageType) {
|
||||
return;
|
||||
}
|
||||
if (widget && widget != target) {
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
info(`Got ${messageType} from widget`);
|
||||
Services.mm.removeMessageListener("paymentContentToChrome", onMessage);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForWidgetReady(widget = null) {
|
||||
return waitForMessageFromWidget("paymentDialogReady", widget);
|
||||
}
|
||||
|
||||
function spawnPaymentDialogTask(paymentDialogFrame, taskFn, args = null) {
|
||||
return ContentTask.spawn(paymentDialogFrame.frameLoader, args, taskFn);
|
||||
}
|
||||
|
||||
async function withMerchantTab(
|
||||
{ browser = gBrowser, url = BLANK_PAGE_URL } = {
|
||||
browser: gBrowser,
|
||||
url: BLANK_PAGE_URL,
|
||||
},
|
||||
taskFn
|
||||
) {
|
||||
await BrowserTestUtils.withNewTab(
|
||||
{
|
||||
gBrowser: browser,
|
||||
url,
|
||||
},
|
||||
taskFn
|
||||
);
|
||||
|
||||
paymentSrv.cleanup(); // Temporary measure until bug 1408234 is fixed.
|
||||
|
||||
await new Promise(resolve => {
|
||||
SpecialPowers.exactGC(resolve);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the privileged payment dialog wrapper document in a new tab and run the
|
||||
* task function.
|
||||
*
|
||||
* @param {string} requestId of the PaymentRequest
|
||||
* @param {Function} taskFn to run in the dialog with the frame as an argument.
|
||||
* @returns {Promise} which resolves when the dialog document is loaded
|
||||
*/
|
||||
function withNewDialogFrame(requestId, taskFn) {
|
||||
async function dialogTabTask(dialogBrowser) {
|
||||
let paymentRequestFrame = dialogBrowser.contentDocument.getElementById(
|
||||
"paymentRequestFrame"
|
||||
);
|
||||
// Ensure the inner frame is loaded
|
||||
await spawnPaymentDialogTask(
|
||||
paymentRequestFrame,
|
||||
async function ensureLoaded() {
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => content.document.readyState == "complete",
|
||||
"Waiting for the unprivileged frame to load"
|
||||
);
|
||||
}
|
||||
);
|
||||
await taskFn(paymentRequestFrame);
|
||||
}
|
||||
|
||||
let args = {
|
||||
gBrowser,
|
||||
url: `chrome://payments/content/paymentDialogWrapper.xul?requestId=${requestId}`,
|
||||
};
|
||||
return BrowserTestUtils.withNewTab(args, dialogTabTask);
|
||||
}
|
||||
|
||||
async function withNewTabInPrivateWindow(args = {}, taskFn) {
|
||||
let privateWin = await BrowserTestUtils.openNewBrowserWindow({
|
||||
private: true,
|
||||
});
|
||||
let tabArgs = Object.assign(args, {
|
||||
browser: privateWin.gBrowser,
|
||||
});
|
||||
await withMerchantTab(tabArgs, taskFn);
|
||||
await BrowserTestUtils.closeWindow(privateWin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a content task inside the inner unprivileged frame of a privileged Payment Request dialog.
|
||||
*
|
||||
* @param {string} requestId
|
||||
* @param {Function} contentTaskFn
|
||||
* @param {object?} [args = null] for the content task
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function spawnTaskInNewDialog(requestId, contentTaskFn, args = null) {
|
||||
return withNewDialogFrame(
|
||||
requestId,
|
||||
async function spawnTaskInNewDialog_tabTask(reqFrame) {
|
||||
await spawnPaymentDialogTask(reqFrame, contentTaskFn, args);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function addAddressRecord(address) {
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
let guid = await formAutofillStorage.addresses.add(address);
|
||||
await onChanged;
|
||||
return guid;
|
||||
}
|
||||
|
||||
async function addCardRecord(card) {
|
||||
let onChanged = TestUtils.topicObserved(
|
||||
"formautofill-storage-changed",
|
||||
(subject, data) => data == "add"
|
||||
);
|
||||
let guid = await formAutofillStorage.creditCards.add(card);
|
||||
await onChanged;
|
||||
return guid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add address and creditCard records to the formautofill store
|
||||
*
|
||||
* @param {array=} addresses - The addresses to add to the formautofill address store
|
||||
* @param {array=} cards - The cards to add to the formautofill creditCards store
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async function addSampleAddressesAndBasicCard(
|
||||
addresses = [PTU.Addresses.TimBL, PTU.Addresses.TimBL2],
|
||||
cards = [PTU.BasicCards.JohnDoe]
|
||||
) {
|
||||
let guids = {};
|
||||
|
||||
for (let i = 0; i < addresses.length; i++) {
|
||||
guids[`address${i + 1}GUID`] = await addAddressRecord(addresses[i]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
guids[`card${i + 1}GUID`] = await addCardRecord(cards[i]);
|
||||
}
|
||||
|
||||
return guids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that an address from autofill storage matches a Payment Request PaymentAddress.
|
||||
* @param {PaymentAddress} paymentAddress
|
||||
* @param {object} storageAddress
|
||||
* @param {string} msg to describe the check
|
||||
*/
|
||||
function checkPaymentAddressMatchesStorageAddress(
|
||||
paymentAddress,
|
||||
storageAddress,
|
||||
msg
|
||||
) {
|
||||
info(msg);
|
||||
let addressLines = storageAddress["street-address"].split("\n");
|
||||
is(
|
||||
paymentAddress.addressLine[0],
|
||||
addressLines[0],
|
||||
"Address line 1 should match"
|
||||
);
|
||||
is(
|
||||
paymentAddress.addressLine[1],
|
||||
addressLines[1],
|
||||
"Address line 2 should match"
|
||||
);
|
||||
is(paymentAddress.country, storageAddress.country, "Country should match");
|
||||
is(
|
||||
paymentAddress.region,
|
||||
storageAddress["address-level1"] || "",
|
||||
"Region should match"
|
||||
);
|
||||
is(
|
||||
paymentAddress.city,
|
||||
storageAddress["address-level2"],
|
||||
"City should match"
|
||||
);
|
||||
is(
|
||||
paymentAddress.postalCode,
|
||||
storageAddress["postal-code"],
|
||||
"Zip code should match"
|
||||
);
|
||||
is(
|
||||
paymentAddress.organization,
|
||||
storageAddress.organization,
|
||||
"Org should match"
|
||||
);
|
||||
is(
|
||||
paymentAddress.recipient,
|
||||
`${storageAddress["given-name"]} ${storageAddress["additional-name"]} ` +
|
||||
`${storageAddress["family-name"]}`,
|
||||
"Recipient name should match"
|
||||
);
|
||||
is(paymentAddress.phone, storageAddress.tel, "Phone should match");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks that a card from autofill storage matches a Payment Request MethodDetails response.
|
||||
* @param {MethodDetails} methodDetails
|
||||
* @param {object} card
|
||||
* @param {string} msg to describe the check
|
||||
*/
|
||||
function checkPaymentMethodDetailsMatchesCard(methodDetails, card, msg) {
|
||||
info(msg);
|
||||
// The card expiry month should be a zero-padded two-digit string.
|
||||
let cardExpiryMonth = card["cc-exp-month"].toString().padStart(2, "0");
|
||||
is(methodDetails.cardholderName, card["cc-name"], "Check cardholderName");
|
||||
is(methodDetails.cardNumber, card["cc-number"], "Check cardNumber");
|
||||
is(methodDetails.expiryMonth, cardExpiryMonth, "Check expiryMonth");
|
||||
is(methodDetails.expiryYear, card["cc-exp-year"], "Check expiryYear");
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a PaymentRequest object with the given parameters, then
|
||||
* run the given merchantTaskFn.
|
||||
*
|
||||
* @param {Object} browser
|
||||
* @param {Object} options
|
||||
* @param {Object} options.methodData
|
||||
* @param {Object} options.details
|
||||
* @param {Object} options.options
|
||||
* @param {Function} options.merchantTaskFn
|
||||
* @returns {Object} References to the window, requestId, and frame
|
||||
*/
|
||||
async function setupPaymentDialog(
|
||||
browser,
|
||||
{ methodData, details, options, merchantTaskFn }
|
||||
) {
|
||||
let dialogReadyPromise = waitForWidgetReady();
|
||||
let { requestId } = await ContentTask.spawn(
|
||||
browser,
|
||||
{
|
||||
methodData,
|
||||
details,
|
||||
options,
|
||||
},
|
||||
merchantTaskFn
|
||||
);
|
||||
ok(requestId, "requestId should be defined");
|
||||
|
||||
// get a reference to the UI dialog and the requestId
|
||||
let [win] = await Promise.all([
|
||||
getPaymentWidget(requestId),
|
||||
dialogReadyPromise,
|
||||
]);
|
||||
ok(win, "Got payment widget");
|
||||
is(win.closed, false, "dialog should not be closed");
|
||||
|
||||
let frame = await getPaymentFrame(win);
|
||||
ok(frame, "Got payment frame");
|
||||
|
||||
await dialogReadyPromise;
|
||||
info("dialog ready");
|
||||
|
||||
await spawnPaymentDialogTask(frame, () => {
|
||||
let elementHeight = element => element.getBoundingClientRect().height;
|
||||
content.isHidden = element => elementHeight(element) == 0;
|
||||
content.isVisible = element => elementHeight(element) > 0;
|
||||
content.fillField = async function fillField(field, value) {
|
||||
// Keep in-sync with the copy in payments_common.js but with EventUtils methods called on a
|
||||
// EventUtils object.
|
||||
field.focus();
|
||||
if (field.localName == "select") {
|
||||
if (field.value == value) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
field.value = value;
|
||||
field.dispatchEvent(
|
||||
new content.window.Event("input", { bubbles: true })
|
||||
);
|
||||
field.dispatchEvent(
|
||||
new content.window.Event("change", { bubbles: true })
|
||||
);
|
||||
return;
|
||||
}
|
||||
while (field.value) {
|
||||
EventUtils.sendKey("BACK_SPACE", content.window);
|
||||
}
|
||||
EventUtils.sendString(value, content.window);
|
||||
};
|
||||
});
|
||||
await injectEventUtilsInContentTask(frame);
|
||||
info("helper functions injected into frame");
|
||||
|
||||
return { win, requestId, frame };
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a merchant tab with the given merchantTaskFn to create a PaymentRequest
|
||||
* and then open the associated PaymentRequest dialog in a new tab and run the
|
||||
* associated dialogTaskFn. The same taskArgs are passed to both functions.
|
||||
*
|
||||
* @param {Function} merchantTaskFn
|
||||
* @param {Function} dialogTaskFn
|
||||
* @param {Object} taskArgs
|
||||
* @param {Object} options
|
||||
* @param {string} options.origin
|
||||
*/
|
||||
async function spawnInDialogForMerchantTask(
|
||||
merchantTaskFn,
|
||||
dialogTaskFn,
|
||||
taskArgs,
|
||||
{ browser, origin = "https://example.com" } = {
|
||||
origin: "https://example.com",
|
||||
}
|
||||
) {
|
||||
await withMerchantTab(
|
||||
{
|
||||
browser,
|
||||
url: origin + BLANK_PAGE_PATH,
|
||||
},
|
||||
async merchBrowser => {
|
||||
let { win, frame } = await setupPaymentDialog(merchBrowser, {
|
||||
...taskArgs,
|
||||
merchantTaskFn,
|
||||
});
|
||||
|
||||
await spawnPaymentDialogTask(frame, dialogTaskFn, taskArgs);
|
||||
spawnPaymentDialogTask(frame, PTU.DialogContentTasks.manuallyClickCancel);
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => win.closed,
|
||||
"dialog should be closed"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function loginAndCompletePayment(frame) {
|
||||
let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true);
|
||||
await spawnPaymentDialogTask(frame, PTU.DialogContentTasks.completePayment);
|
||||
await osKeyStoreLoginShown;
|
||||
}
|
||||
|
||||
async function setupFormAutofillStorage() {
|
||||
await formAutofillStorage.initialize();
|
||||
}
|
||||
|
||||
function cleanupFormAutofillStorage() {
|
||||
formAutofillStorage.addresses.removeAll();
|
||||
formAutofillStorage.creditCards.removeAll();
|
||||
}
|
||||
|
||||
add_task(async function setup_head() {
|
||||
SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) {
|
||||
if (msg.isWarning || !msg.errorMessage) {
|
||||
// Ignore warnings and non-errors.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.category == "CSP_CSPViolationWithURI" &&
|
||||
msg.errorMessage.includes("at inline")
|
||||
) {
|
||||
// Ignore unknown CSP error.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.message &&
|
||||
msg.message.match(/docShell is null.*BrowserUtils.jsm/)
|
||||
) {
|
||||
// Bug 1478142 - Console spam from the Find Toolbar.
|
||||
return;
|
||||
}
|
||||
if (msg.message && msg.message.match(/PrioEncoder is not defined/)) {
|
||||
// Bug 1492638 - Console spam from TelemetrySession.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.message &&
|
||||
msg.message.match(/devicePixelRatio.*FaviconLoader.jsm/)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.errorMessage == "AbortError: The operation was aborted. " &&
|
||||
msg.sourceName == "" &&
|
||||
msg.lineNumber == 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
ok(false, msg.message || msg.errorMessage);
|
||||
});
|
||||
OSKeyStoreTestUtils.setup();
|
||||
await setupFormAutofillStorage();
|
||||
registerCleanupFunction(async function cleanup() {
|
||||
paymentSrv.cleanup();
|
||||
cleanupFormAutofillStorage();
|
||||
await OSKeyStoreTestUtils.cleanup();
|
||||
Services.prefs.clearUserPref(RESPONSE_TIMEOUT_PREF);
|
||||
Services.prefs.clearUserPref(SAVE_CREDITCARD_DEFAULT_PREF);
|
||||
Services.prefs.clearUserPref(SAVE_ADDRESS_DEFAULT_PREF);
|
||||
SpecialPowers.postConsoleSentinel();
|
||||
// CreditCard.jsm is imported into the global scope. It needs to be deleted
|
||||
// else it outlives the test and is reported as a leak.
|
||||
delete window.CreditCard;
|
||||
});
|
||||
});
|
||||
|
||||
function deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
async function selectPaymentDialogShippingAddressByCountry(frame, country) {
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
PTU.DialogContentTasks.selectShippingAddressByCountry,
|
||||
country
|
||||
);
|
||||
}
|
||||
|
||||
async function navigateToAddAddressPage(frame, aOptions = {}) {
|
||||
ok(aOptions.initialPageId, "initialPageId option supplied");
|
||||
ok(aOptions.addressPageId, "addressPageId option supplied");
|
||||
ok(aOptions.addLinkSelector, "addLinkSelector option supplied");
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async options => {
|
||||
let { PaymentTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
info("navigateToAddAddressPage: check we're on the expected page first");
|
||||
await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
info(
|
||||
"current page state: " +
|
||||
state.page.id +
|
||||
" waiting for: " +
|
||||
options.initialPageId
|
||||
);
|
||||
return state.page.id == options.initialPageId;
|
||||
},
|
||||
"Check initial page state"
|
||||
);
|
||||
|
||||
// click through to add/edit address page
|
||||
info("navigateToAddAddressPage: click the link");
|
||||
let addLink = content.document.querySelector(options.addLinkSelector);
|
||||
addLink.click();
|
||||
|
||||
info("navigateToAddAddressPage: wait for address page");
|
||||
await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == options.addressPageId && !state.page.guid;
|
||||
},
|
||||
"Check add page state"
|
||||
);
|
||||
},
|
||||
aOptions
|
||||
);
|
||||
}
|
||||
|
||||
async function navigateToAddShippingAddressPage(frame, aOptions = {}) {
|
||||
let options = Object.assign(
|
||||
{
|
||||
addLinkSelector:
|
||||
'address-picker[selected-state-key="selectedShippingAddress"] .add-link',
|
||||
initialPageId: "payment-summary",
|
||||
addressPageId: "shipping-address-page",
|
||||
},
|
||||
aOptions
|
||||
);
|
||||
await navigateToAddAddressPage(frame, options);
|
||||
}
|
||||
|
||||
async function fillInBillingAddressForm(frame, aAddress, aOptions = {}) {
|
||||
// For now billing and shipping address forms have the same fields but that may
|
||||
// change so use separarate helpers.
|
||||
let address = Object.assign({}, aAddress);
|
||||
// Email isn't used on address forms, only payer/contact ones.
|
||||
delete address.email;
|
||||
let options = Object.assign(
|
||||
{
|
||||
addressPageId: "billing-address-page",
|
||||
expectedSelectedStateKey: ["basic-card-page", "billingAddressGUID"],
|
||||
},
|
||||
aOptions
|
||||
);
|
||||
return fillInAddressForm(frame, address, options);
|
||||
}
|
||||
|
||||
async function fillInShippingAddressForm(frame, aAddress, aOptions) {
|
||||
let address = Object.assign({}, aAddress);
|
||||
// Email isn't used on address forms, only payer/contact ones.
|
||||
delete address.email;
|
||||
return fillInAddressForm(frame, address, {
|
||||
expectedSelectedStateKey: ["selectedShippingAddress"],
|
||||
...aOptions,
|
||||
});
|
||||
}
|
||||
|
||||
async function fillInPayerAddressForm(frame, aAddress) {
|
||||
let address = Object.assign({}, aAddress);
|
||||
let payerFields = [
|
||||
"given-name",
|
||||
"additional-name",
|
||||
"family-name",
|
||||
"tel",
|
||||
"email",
|
||||
];
|
||||
for (let fieldName of Object.keys(address)) {
|
||||
if (payerFields.includes(fieldName)) {
|
||||
continue;
|
||||
}
|
||||
delete address[fieldName];
|
||||
}
|
||||
return fillInAddressForm(frame, address, {
|
||||
expectedSelectedStateKey: ["selectedPayerAddress"],
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} frame
|
||||
* @param {object} aAddress
|
||||
* @param {object} [aOptions = {}]
|
||||
* @param {boolean} [aOptions.setPersistCheckedValue = undefined] How to set the persist checkbox.
|
||||
* @param {string[]} [expectedSelectedStateKey = undefined] The expected selectedStateKey for
|
||||
address-page.
|
||||
*/
|
||||
async function fillInAddressForm(frame, aAddress, aOptions = {}) {
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async args => {
|
||||
let { address, options = {} } = args;
|
||||
let { requestStore } = Cu.waiveXrays(
|
||||
content.document.querySelector("payment-dialog")
|
||||
);
|
||||
let currentState = requestStore.getState();
|
||||
let addressForm = content.document.getElementById(currentState.page.id);
|
||||
ok(
|
||||
addressForm,
|
||||
"found the addressForm: " + addressForm.getAttribute("id")
|
||||
);
|
||||
|
||||
if (options.expectedSelectedStateKey) {
|
||||
Assert.deepEqual(
|
||||
addressForm.getAttribute("selected-state-key").split("|"),
|
||||
options.expectedSelectedStateKey,
|
||||
"Check address page selectedStateKey"
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof address.country != "undefined") {
|
||||
// Set the country first so that the appropriate fields are visible.
|
||||
let countryField = addressForm.querySelector("#country");
|
||||
ok(!countryField.disabled, "Country Field shouldn't be disabled");
|
||||
await content.fillField(countryField, address.country);
|
||||
is(
|
||||
countryField.value,
|
||||
address.country,
|
||||
"country value is correct after fillField"
|
||||
);
|
||||
}
|
||||
|
||||
// fill the form
|
||||
info(
|
||||
"fillInAddressForm: fill the form with address: " +
|
||||
JSON.stringify(address)
|
||||
);
|
||||
for (let [key, val] of Object.entries(address)) {
|
||||
let field = addressForm.querySelector(`#${key}`);
|
||||
if (!field) {
|
||||
ok(false, `${key} field not found`);
|
||||
}
|
||||
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
|
||||
await content.fillField(field, val);
|
||||
is(field.value, val, `${key} value is correct after fillField`);
|
||||
}
|
||||
let persistCheckbox = Cu.waiveXrays(
|
||||
addressForm.querySelector(".persist-checkbox")
|
||||
);
|
||||
// only touch the checked state if explicitly told to in the options
|
||||
if (options.hasOwnProperty("setPersistCheckedValue")) {
|
||||
info(
|
||||
"fillInAddressForm: Manually setting the persist checkbox checkedness to: " +
|
||||
options.setPersistCheckedValue
|
||||
);
|
||||
Cu.waiveXrays(persistCheckbox).checked = options.setPersistCheckedValue;
|
||||
}
|
||||
info(
|
||||
`fillInAddressForm, persistCheckbox.checked: ${persistCheckbox.checked}`
|
||||
);
|
||||
},
|
||||
{ address: aAddress, options: aOptions }
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyPersistCheckbox(frame, aOptions = {}) {
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async args => {
|
||||
let { options = {} } = args;
|
||||
// ensure card/address is persisted or not based on the temporary option given
|
||||
info("verifyPersistCheckbox, got options: " + JSON.stringify(options));
|
||||
let persistCheckbox = Cu.waiveXrays(
|
||||
content.document.querySelector(options.checkboxSelector)
|
||||
);
|
||||
|
||||
if (options.isEditing) {
|
||||
ok(
|
||||
persistCheckbox.hidden,
|
||||
"checkbox should be hidden when editing a record"
|
||||
);
|
||||
} else {
|
||||
ok(
|
||||
!persistCheckbox.hidden,
|
||||
"checkbox should be visible when adding a new record"
|
||||
);
|
||||
is(
|
||||
persistCheckbox.checked,
|
||||
options.expectPersist,
|
||||
`persist checkbox state is expected to be ${options.expectPersist}`
|
||||
);
|
||||
}
|
||||
},
|
||||
{ options: aOptions }
|
||||
);
|
||||
}
|
||||
|
||||
async function verifyCardNetwork(frame, aOptions = {}) {
|
||||
aOptions.supportedNetworks = CreditCard.SUPPORTED_NETWORKS;
|
||||
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async args => {
|
||||
let { options = {} } = args;
|
||||
// ensure the network picker is visible, has the right contents and expected value
|
||||
let networkSelect = Cu.waiveXrays(
|
||||
content.document.querySelector(options.networkSelector)
|
||||
);
|
||||
ok(
|
||||
content.isVisible(networkSelect),
|
||||
"The network selector should always be visible"
|
||||
);
|
||||
is(
|
||||
networkSelect.childElementCount,
|
||||
options.supportedNetworks.length + 1,
|
||||
"Should have one more than the number of supported networks"
|
||||
);
|
||||
is(
|
||||
networkSelect.children[0].value,
|
||||
"",
|
||||
"The first option should be the blank/empty option"
|
||||
);
|
||||
is(
|
||||
networkSelect.value,
|
||||
options.expectedNetwork,
|
||||
`The network picker should have the expected value`
|
||||
);
|
||||
},
|
||||
{ options: aOptions }
|
||||
);
|
||||
}
|
||||
|
||||
async function submitAddressForm(
|
||||
frame,
|
||||
aAddress,
|
||||
aOptions = {
|
||||
nextPageId: "payment-summary",
|
||||
}
|
||||
) {
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async args => {
|
||||
let { options = {} } = args;
|
||||
let nextPageId = options.nextPageId || "payment-summary";
|
||||
let { PaymentTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
let oldState = await PaymentTestUtils.DialogContentUtils.getCurrentState(
|
||||
content
|
||||
);
|
||||
let pageId = oldState.page.id;
|
||||
|
||||
// submit the form to return to summary page
|
||||
content.document.querySelector(`#${pageId} button.primary`).click();
|
||||
|
||||
let currState = await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == nextPageId;
|
||||
},
|
||||
`submitAddressForm: Switched back to ${nextPageId}`
|
||||
);
|
||||
|
||||
let savedCount = Object.keys(currState.savedAddresses).length;
|
||||
let tempCount = Object.keys(currState.tempAddresses).length;
|
||||
let oldSavedCount = Object.keys(oldState.savedAddresses).length;
|
||||
let oldTempCount = Object.keys(oldState.tempAddresses).length;
|
||||
|
||||
if (options.isEditing) {
|
||||
is(tempCount, oldTempCount, "tempAddresses count didn't change");
|
||||
is(savedCount, oldSavedCount, "savedAddresses count didn't change");
|
||||
} else if (options.expectPersist) {
|
||||
is(tempCount, oldTempCount, "tempAddresses count didn't change");
|
||||
is(savedCount, oldSavedCount + 1, "Entry added to savedAddresses");
|
||||
} else {
|
||||
is(tempCount, oldTempCount + 1, "Entry added to tempAddresses");
|
||||
is(savedCount, oldSavedCount, "savedAddresses count didn't change");
|
||||
}
|
||||
},
|
||||
{ address: aAddress, options: aOptions }
|
||||
);
|
||||
}
|
||||
|
||||
async function manuallyAddShippingAddress(frame, aAddress, aOptions = {}) {
|
||||
let options = Object.assign(
|
||||
{
|
||||
expectPersist: true,
|
||||
isEditing: false,
|
||||
},
|
||||
aOptions,
|
||||
{
|
||||
checkboxSelector: "#shipping-address-page .persist-checkbox",
|
||||
}
|
||||
);
|
||||
await navigateToAddShippingAddressPage(frame);
|
||||
info(
|
||||
"manuallyAddShippingAddress, fill in address form with options: " +
|
||||
JSON.stringify(options)
|
||||
);
|
||||
await fillInShippingAddressForm(frame, aAddress, options);
|
||||
info(
|
||||
"manuallyAddShippingAddress, verifyPersistCheckbox with options: " +
|
||||
JSON.stringify(options)
|
||||
);
|
||||
await verifyPersistCheckbox(frame, options);
|
||||
await submitAddressForm(frame, aAddress, options);
|
||||
}
|
||||
|
||||
async function navigateToAddCardPage(
|
||||
frame,
|
||||
aOptions = {
|
||||
addLinkSelector: "payment-method-picker .add-link",
|
||||
}
|
||||
) {
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async options => {
|
||||
let { PaymentTestUtils } = ChromeUtils.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm"
|
||||
);
|
||||
|
||||
// check were on the summary page first
|
||||
await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return !state.page.id || state.page.id == "payment-summary";
|
||||
},
|
||||
"Check summary page state"
|
||||
);
|
||||
|
||||
// click through to add/edit card page
|
||||
let addLink = content.document.querySelector(options.addLinkSelector);
|
||||
addLink.click();
|
||||
|
||||
// wait for card page
|
||||
await PaymentTestUtils.DialogContentUtils.waitForState(
|
||||
content,
|
||||
state => {
|
||||
return state.page.id == "basic-card-page";
|
||||
},
|
||||
"Check add/edit page state"
|
||||
);
|
||||
},
|
||||
aOptions
|
||||
);
|
||||
}
|
||||
|
||||
async function fillInCardForm(frame, aCard, aOptions = {}) {
|
||||
await spawnPaymentDialogTask(
|
||||
frame,
|
||||
async args => {
|
||||
let { card, options = {} } = args;
|
||||
|
||||
// fill the form
|
||||
info("fillInCardForm: fill the form with card: " + JSON.stringify(card));
|
||||
for (let [key, val] of Object.entries(card)) {
|
||||
let field = content.document.getElementById(key);
|
||||
if (!field) {
|
||||
ok(false, `${key} field not found`);
|
||||
}
|
||||
ok(!field.disabled, `Field #${key} shouldn't be disabled`);
|
||||
// Reset the value first so that we properly handle typing the value
|
||||
// already selected which may select another option with the same prefix.
|
||||
field.value = "";
|
||||
ok(!field.value, "Field value should be reset before typing");
|
||||
field.blur();
|
||||
field.focus();
|
||||
// Using waitForEvent here causes the test to hang, but
|
||||
// waitForCondition and checking activeElement does the trick. The root cause
|
||||
// of this should be investigated further.
|
||||
await ContentTaskUtils.waitForCondition(
|
||||
() => field == content.document.activeElement,
|
||||
`Waiting for field #${key} to get focus`
|
||||
);
|
||||
if (key == "billingAddressGUID") {
|
||||
// Can't type the value in, press Down until the value is found
|
||||
content.fillField(field, val);
|
||||
} else {
|
||||
// cc-exp-* fields are numbers so convert to strings and pad left with 0
|
||||
let fillValue = val.toString().padStart(2, "0");
|
||||
EventUtils.synthesizeKey(
|
||||
fillValue,
|
||||
{},
|
||||
Cu.waiveXrays(content.window)
|
||||
);
|
||||
}
|
||||
// cc-exp-* field values are not padded, so compare with unpadded string.
|
||||
is(
|
||||
field.value,
|
||||
val.toString(),
|
||||
`${key} value is correct after sendString`
|
||||
);
|
||||
}
|
||||
|
||||
info(
|
||||
[...content.document.getElementById("cc-exp-year").options]
|
||||
.map(op => op.label)
|
||||
.join(",")
|
||||
);
|
||||
|
||||
let persistCheckbox = content.document.querySelector(
|
||||
options.checkboxSelector
|
||||
);
|
||||
// only touch the checked state if explicitly told to in the options
|
||||
if (options.hasOwnProperty("setPersistCheckedValue")) {
|
||||
info(
|
||||
"fillInCardForm: Manually setting the persist checkbox checkedness to: " +
|
||||
options.setPersistCheckedValue
|
||||
);
|
||||
Cu.waiveXrays(persistCheckbox).checked = options.setPersistCheckedValue;
|
||||
}
|
||||
},
|
||||
{ card: aCard, options: aOptions }
|
||||
);
|
||||
}
|
||||
|
||||
// The JSDoc validator does not support @returns tags in abstract functions or
|
||||
// star functions without return statements.
|
||||
/* eslint-disable valid-jsdoc */
|
||||
/**
|
||||
* Inject `EventUtils` helpers into ContentTask scope.
|
||||
*
|
||||
* This helper is automatically exposed to mochitest browser tests,
|
||||
* but is missing from content task scope.
|
||||
* You should call this method only once per <browser> tag
|
||||
*
|
||||
* @param {xul:browser} browser
|
||||
* Reference to the browser in which we load content task
|
||||
*/
|
||||
/* eslint-enable valid-jsdoc */
|
||||
async function injectEventUtilsInContentTask(browser) {
|
||||
await spawnPaymentDialogTask(browser, async function injectEventUtils() {
|
||||
if ("EventUtils" in this) {
|
||||
return;
|
||||
}
|
||||
|
||||
const EventUtils = (this.EventUtils = {});
|
||||
|
||||
EventUtils.window = {};
|
||||
EventUtils.parent = EventUtils.window;
|
||||
/* eslint-disable camelcase */
|
||||
EventUtils._EU_Ci = Ci;
|
||||
EventUtils._EU_Cc = Cc;
|
||||
/* eslint-enable camelcase */
|
||||
// EventUtils' `sendChar` function relies on the navigator to synthetize events.
|
||||
EventUtils.navigator = content.navigator;
|
||||
EventUtils.KeyboardEvent = content.KeyboardEvent;
|
||||
|
||||
Services.scriptloader.loadSubScript(
|
||||
"chrome://mochikit/content/tests/SimpleTest/EventUtils.js",
|
||||
EventUtils
|
||||
);
|
||||
});
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
[DEFAULT]
|
||||
# This manifest mostly exists so that the support-files below can be referenced
|
||||
# from a relative path of formautofill/* from the tests in the above directory
|
||||
# to resemble the layout in the shipped JAR file.
|
||||
support-files =
|
||||
../../../../../../browser/extensions/formautofill/content/editCreditCard.xhtml
|
||||
../../../../../../browser/extensions/formautofill/content/editAddress.xhtml
|
||||
|
||||
skip-if = true # Bug 1446164
|
||||
[test_editCreditCard.html]
|
@ -1,34 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test that editCreditCard.xhtml is accessible for tests in the parent directory.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test that editCreditCard.xhtml is accessible</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<iframe id="editCreditCard" src="editCreditCard.xhtml"></iframe>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="application/javascript">
|
||||
|
||||
add_task(async function test_editCreditCard() {
|
||||
let editCreditCard = document.getElementById("editCreditCard").contentWindow;
|
||||
await SimpleTest.promiseFocus(editCreditCard);
|
||||
ok(editCreditCard.document.getElementById("form"), "Check form is present");
|
||||
ok(editCreditCard.document.getElementById("cc-number"), "Check cc-number is present");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,37 +0,0 @@
|
||||
[DEFAULT]
|
||||
support-files =
|
||||
!/browser/extensions/formautofill/content/editAddress.xhtml
|
||||
!/browser/extensions/formautofill/content/editCreditCard.xhtml
|
||||
../../../../../browser/extensions/formautofill/content/autofillEditForms.js
|
||||
../../../../../browser/extensions/formautofill/skin/shared/editDialog-shared.css
|
||||
../../../../../testing/modules/sinon-7.2.7.js
|
||||
# paymentRequest.xhtml is needed for `importDialogDependencies` so that the relative paths of
|
||||
# formautofill/edit*.xhtml work from the *-form elements in paymentRequest.xhtml.
|
||||
../../res/paymentRequest.xhtml
|
||||
../../res/**
|
||||
payments_common.js
|
||||
skip-if = true || !e10s # Bug 1515048 - Disable for now. Bug 1365964 - Payment Request isn't implemented for non-e10s.
|
||||
|
||||
[test_accepted_cards.html]
|
||||
[test_address_form.html]
|
||||
[test_address_option.html]
|
||||
skip-if = os == "linux" || os == "win" # Bug 1493216
|
||||
[test_address_picker.html]
|
||||
[test_basic_card_form.html]
|
||||
skip-if = debug || asan # Bug 1493349
|
||||
[test_basic_card_option.html]
|
||||
[test_billing_address_picker.html]
|
||||
[test_completion_error_page.html]
|
||||
[test_currency_amount.html]
|
||||
[test_labelled_checkbox.html]
|
||||
[test_order_details.html]
|
||||
[test_payer_address_picker.html]
|
||||
[test_payment_dialog.html]
|
||||
[test_payment_dialog_required_top_level_items.html]
|
||||
[test_payment_details_item.html]
|
||||
[test_payment_method_picker.html]
|
||||
[test_rich_select.html]
|
||||
[test_shipping_option_picker.html]
|
||||
[test_ObservedPropertiesMixin.html]
|
||||
[test_PaymentsStore.html]
|
||||
[test_PaymentStateSubscriberMixin.html]
|
@ -1,154 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
/* exported asyncElementRendered, promiseStateChange, promiseContentToChromeMessage, deepClone,
|
||||
PTU, registerConsoleFilter, fillField, importDialogDependencies */
|
||||
|
||||
const PTU = SpecialPowers.Cu.import(
|
||||
"resource://testing-common/PaymentTestUtils.jsm",
|
||||
{}
|
||||
).PaymentTestUtils;
|
||||
|
||||
/**
|
||||
* A helper to await on while waiting for an asynchronous rendering of a Custom
|
||||
* Element.
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function asyncElementRendered() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function promiseStateChange(store) {
|
||||
return new Promise(resolve => {
|
||||
store.subscribe({
|
||||
stateChangeCallback(state) {
|
||||
store.unsubscribe(this);
|
||||
resolve(state);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a message of `messageType` from content to chrome and resolve with the event details.
|
||||
* @param {string} messageType of the expected message
|
||||
* @returns {Promise} when the message is dispatched
|
||||
*/
|
||||
function promiseContentToChromeMessage(messageType) {
|
||||
return new Promise(resolve => {
|
||||
document.addEventListener("paymentContentToChrome", function onCToC(event) {
|
||||
if (event.detail.messageType != messageType) {
|
||||
return;
|
||||
}
|
||||
document.removeEventListener("paymentContentToChrome", onCToC);
|
||||
resolve(event.detail);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the templates and stylesheets from the real shipping dialog to avoid
|
||||
* duplication in the tests.
|
||||
* @param {HTMLIFrameElement} templateFrame - Frame to copy the resources from
|
||||
* @param {HTMLElement} destinationEl - Where to append the copied resources
|
||||
*/
|
||||
function importDialogDependencies(templateFrame, destinationEl) {
|
||||
let templates = templateFrame.contentDocument.querySelectorAll("template");
|
||||
isnot(templates, null, "Check some templates found");
|
||||
for (let template of templates) {
|
||||
let imported = document.importNode(template, true);
|
||||
destinationEl.appendChild(imported);
|
||||
}
|
||||
|
||||
let baseURL = new URL("../../res/", window.location.href);
|
||||
let stylesheetLinks = templateFrame.contentDocument.querySelectorAll(
|
||||
"link[rel~='stylesheet']"
|
||||
);
|
||||
for (let stylesheet of stylesheetLinks) {
|
||||
let imported = document.importNode(stylesheet, true);
|
||||
imported.href = new URL(imported.getAttribute("href"), baseURL);
|
||||
destinationEl.appendChild(imported);
|
||||
}
|
||||
}
|
||||
|
||||
function deepClone(obj) {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} field
|
||||
* @param {string} value
|
||||
* @note This is async in case we need to make it async to handle focus in the future.
|
||||
* @note Keep in sync with the copy in head.js
|
||||
*/
|
||||
async function fillField(field, value) {
|
||||
field.focus();
|
||||
if (field.localName == "select") {
|
||||
if (field.value == value) {
|
||||
// Do nothing
|
||||
return;
|
||||
}
|
||||
field.value = value;
|
||||
field.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
field.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return;
|
||||
}
|
||||
while (field.value) {
|
||||
sendKey("BACK_SPACE");
|
||||
}
|
||||
sendString(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* If filterFunction is a function which returns true given a console message
|
||||
* then the test won't fail from that message.
|
||||
*/
|
||||
let filterFunction = null;
|
||||
function registerConsoleFilter(filterFn) {
|
||||
filterFunction = filterFn;
|
||||
}
|
||||
|
||||
// Listen for errors to fail tests
|
||||
SpecialPowers.registerConsoleListener(function onConsoleMessage(msg) {
|
||||
if (
|
||||
msg.isWarning ||
|
||||
!msg.errorMessage ||
|
||||
msg.errorMessage == "paymentRequest.xhtml:"
|
||||
) {
|
||||
// Ignore warnings and non-errors.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.category == "CSP_CSPViolationWithURI" &&
|
||||
msg.errorMessage.includes("at inline")
|
||||
) {
|
||||
// Ignore unknown CSP error.
|
||||
return;
|
||||
}
|
||||
if (
|
||||
msg.message &&
|
||||
msg.message.includes("Security Error: Content at http://mochi.test:8888")
|
||||
) {
|
||||
// Check for same-origin policy violations and ignore specific errors
|
||||
if (
|
||||
msg.message.includes("icon-credit-card-generic.svg") ||
|
||||
msg.message.includes("accepted-cards.css") ||
|
||||
msg.message.includes("editDialog-shared.css") ||
|
||||
msg.message.includes("editAddress.css") ||
|
||||
msg.message.includes("editDialog.css") ||
|
||||
msg.message.includes("editCreditCard.css")
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (msg.message == "SENTINEL") {
|
||||
filterFunction = null;
|
||||
}
|
||||
if (filterFunction && filterFunction(msg)) {
|
||||
return;
|
||||
}
|
||||
ok(false, msg.message || msg.errorMessage);
|
||||
});
|
||||
|
||||
SimpleTest.registerCleanupFunction(function cleanup() {
|
||||
SpecialPowers.postConsoleSentinel();
|
||||
});
|
@ -1,116 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the ObservedPropertiesMixin
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the ObservedPropertiesMixin</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<test-element id="el1" one="foo" two-word="bar"></test-element>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the ObservedPropertiesMixin **/
|
||||
|
||||
import ObservedPropertiesMixin from "../../res/mixins/ObservedPropertiesMixin.js";
|
||||
|
||||
class TestElement extends ObservedPropertiesMixin(HTMLElement) {
|
||||
static get observedAttributes() {
|
||||
return ["one", "two-word"];
|
||||
}
|
||||
|
||||
render() {
|
||||
this.textContent = JSON.stringify({
|
||||
one: this.one,
|
||||
twoWord: this.twoWord,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("test-element", TestElement);
|
||||
let el1 = document.getElementById("el1");
|
||||
|
||||
add_task(async function test_default_properties() {
|
||||
is(el1.one, "foo", "Check .one matches @one");
|
||||
is(el1.twoWord, "bar", "Check .twoWord matches @two-word");
|
||||
let expected = `{"one":"foo","twoWord":"bar"}`;
|
||||
is(el1.textContent, expected, "Check textContent");
|
||||
});
|
||||
|
||||
add_task(async function test_set_properties() {
|
||||
el1.one = "a";
|
||||
el1.twoWord = "b";
|
||||
is(el1.one, "a", "Check .one value");
|
||||
is(el1.getAttribute("one"), "a", "Check @one");
|
||||
is(el1.twoWord, "b", "Check .twoWord value");
|
||||
is(el1.getAttribute("two-word"), "b", "Check @two-word");
|
||||
let expected = `{"one":"a","twoWord":"b"}`;
|
||||
await asyncElementRendered();
|
||||
is(el1.textContent, expected, "Check textContent");
|
||||
});
|
||||
|
||||
add_task(async function test_set_attributes() {
|
||||
el1.setAttribute("one", "X");
|
||||
el1.setAttribute("two-word", "Y");
|
||||
is(el1.one, "X", "Check .one value");
|
||||
is(el1.getAttribute("one"), "X", "Check @one");
|
||||
is(el1.twoWord, "Y", "Check .twoWord value");
|
||||
is(el1.getAttribute("two-word"), "Y", "Check @two-word");
|
||||
let expected = `{"one":"X","twoWord":"Y"}`;
|
||||
await asyncElementRendered();
|
||||
is(el1.textContent, expected, "Check textContent");
|
||||
});
|
||||
|
||||
add_task(async function test_async_render() {
|
||||
// Setup
|
||||
el1.setAttribute("one", "1");
|
||||
el1.setAttribute("two-word", "2");
|
||||
await asyncElementRendered(); // Wait for the async render
|
||||
|
||||
el1.setAttribute("one", "new1");
|
||||
|
||||
is(el1.one, "new1", "Check .one value");
|
||||
is(el1.getAttribute("one"), "new1", "Check @one");
|
||||
is(el1.twoWord, "2", "Check .twoWord value");
|
||||
is(el1.getAttribute("two-word"), "2", "Check @two-word");
|
||||
let expected = `{"one":"1","twoWord":"2"}`;
|
||||
is(el1.textContent, expected, "Check textContent is still old value due to async rendering");
|
||||
await asyncElementRendered();
|
||||
expected = `{"one":"new1","twoWord":"2"}`;
|
||||
is(el1.textContent, expected, "Check textContent now has the new value");
|
||||
});
|
||||
|
||||
add_task(async function test_batched_render() {
|
||||
// Setup
|
||||
el1.setAttribute("one", "1");
|
||||
el1.setAttribute("two-word", "2");
|
||||
await asyncElementRendered();
|
||||
|
||||
el1.setAttribute("one", "new1");
|
||||
el1.setAttribute("two-word", "new2");
|
||||
|
||||
is(el1.one, "new1", "Check .one value");
|
||||
is(el1.getAttribute("one"), "new1", "Check @one");
|
||||
is(el1.twoWord, "new2", "Check .twoWord value");
|
||||
is(el1.getAttribute("two-word"), "new2", "Check @two-word");
|
||||
let expected = `{"one":"1","twoWord":"2"}`;
|
||||
is(el1.textContent, expected, "Check textContent is still old value due to async rendering");
|
||||
await asyncElementRendered();
|
||||
expected = `{"one":"new1","twoWord":"new2"}`;
|
||||
is(el1.textContent, expected, "Check textContent now has the new value");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,79 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the PaymentStateSubscriberMixin
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the PaymentStateSubscriberMixin</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="sinon-7.2.7.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<test-element id="el1"></test-element>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the PaymentStateSubscriberMixin **/
|
||||
|
||||
/* global sinon */
|
||||
|
||||
import PaymentStateSubscriberMixin from "../../res/mixins/PaymentStateSubscriberMixin.js";
|
||||
|
||||
class TestElement extends PaymentStateSubscriberMixin(HTMLElement) {
|
||||
render(state) {
|
||||
this.textContent = JSON.stringify(state);
|
||||
}
|
||||
}
|
||||
|
||||
// We must spy on the prototype by creating the instance in order to test Custom Element reactions.
|
||||
sinon.spy(TestElement.prototype, "disconnectedCallback");
|
||||
|
||||
customElements.define("test-element", TestElement);
|
||||
let el1 = document.getElementById("el1");
|
||||
|
||||
sinon.spy(el1, "render");
|
||||
sinon.spy(el1, "stateChangeCallback");
|
||||
|
||||
add_task(async function test_initialState() {
|
||||
let parsedState = JSON.parse(el1.textContent);
|
||||
ok(!!parsedState.request, "Check initial state contains `request`");
|
||||
ok(!!parsedState.savedAddresses, "Check initial state contains `savedAddresses`");
|
||||
ok(!!parsedState.savedBasicCards, "Check initial state contains `savedBasicCards`");
|
||||
});
|
||||
|
||||
add_task(async function test_async_batched_render() {
|
||||
el1.requestStore.setState({a: 1});
|
||||
el1.requestStore.setState({b: 2});
|
||||
await asyncElementRendered();
|
||||
ok(el1.stateChangeCallback.calledOnce, "stateChangeCallback called once");
|
||||
ok(el1.render.calledOnce, "render called once");
|
||||
|
||||
let parsedState = JSON.parse(el1.textContent);
|
||||
is(parsedState.a, 1, "Check a");
|
||||
is(parsedState.b, 2, "Check b");
|
||||
});
|
||||
|
||||
add_task(async function test_disconnect() {
|
||||
el1.disconnectedCallback.reset();
|
||||
el1.render.reset();
|
||||
el1.stateChangeCallback.reset();
|
||||
el1.remove();
|
||||
ok(el1.disconnectedCallback.calledOnce, "disconnectedCallback called once");
|
||||
await el1.requestStore.setState({a: 3});
|
||||
await asyncElementRendered();
|
||||
ok(el1.stateChangeCallback.notCalled, "stateChangeCallback not called");
|
||||
ok(el1.render.notCalled, "render not called");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,168 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the PaymentsStore
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the PaymentsStore</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
|
||||
<script src="sinon-7.2.7.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the PaymentsStore **/
|
||||
|
||||
/* global sinon */
|
||||
|
||||
import PaymentsStore from "../../res/PaymentsStore.js";
|
||||
|
||||
function assert_throws(block, expectedError, message) {
|
||||
let actual;
|
||||
try {
|
||||
block();
|
||||
} catch (e) {
|
||||
actual = e;
|
||||
}
|
||||
ok(actual, "Expecting exception: " + message);
|
||||
ok(actual instanceof expectedError,
|
||||
`Check error type is ${expectedError.prototype.name}: ${message}`);
|
||||
}
|
||||
|
||||
add_task(async function test_defaultState() {
|
||||
ok(!!PaymentsStore, "Check PaymentsStore import");
|
||||
let ps = new PaymentsStore({
|
||||
foo: "bar",
|
||||
});
|
||||
|
||||
let state = ps.getState();
|
||||
ok(!!state, "Check state is truthy");
|
||||
is(state.foo, "bar", "Check .foo");
|
||||
|
||||
assert_throws(() => state.foo = "new", TypeError, "Assigning to existing prop. should throw");
|
||||
assert_throws(() => state.other = "something", TypeError, "Adding a new prop. should throw");
|
||||
assert_throws(() => delete state.foo, TypeError, "Deleting a prop. should throw");
|
||||
});
|
||||
|
||||
add_task(async function test_setState() {
|
||||
let ps = new PaymentsStore({});
|
||||
|
||||
ps.setState({
|
||||
one: "one",
|
||||
});
|
||||
let state = ps.getState();
|
||||
is(Object.keys(state).length, 1, "Should only have 1 prop. set");
|
||||
is(state.one, "one", "Check .one");
|
||||
|
||||
ps.setState({
|
||||
two: 2,
|
||||
});
|
||||
state = ps.getState();
|
||||
is(Object.keys(state).length, 2, "Should have 2 props. set");
|
||||
is(state.one, "one", "Check .one");
|
||||
is(state.two, 2, "Check .two");
|
||||
|
||||
ps.setState({
|
||||
one: "a",
|
||||
two: "b",
|
||||
});
|
||||
state = ps.getState();
|
||||
is(state.one, "a", "Check .one");
|
||||
is(state.two, "b", "Check .two");
|
||||
|
||||
info("check consecutive setState for the same prop");
|
||||
ps.setState({
|
||||
one: "c",
|
||||
});
|
||||
ps.setState({
|
||||
one: "d",
|
||||
});
|
||||
state = ps.getState();
|
||||
is(Object.keys(state).length, 2, "Should have 2 props. set");
|
||||
is(state.one, "d", "Check .one");
|
||||
is(state.two, "b", "Check .two");
|
||||
});
|
||||
|
||||
add_task(async function test_subscribe_unsubscribe() {
|
||||
let ps = new PaymentsStore({});
|
||||
let subscriber = {
|
||||
stateChangePromise: null,
|
||||
_stateChangeResolver: null,
|
||||
|
||||
reset() {
|
||||
this.stateChangePromise = new Promise(resolve => {
|
||||
this._stateChangeResolver = resolve;
|
||||
});
|
||||
},
|
||||
|
||||
stateChangeCallback(state) {
|
||||
this._stateChangeResolver(state);
|
||||
this.stateChangePromise = new Promise(resolve => {
|
||||
this._stateChangeResolver = resolve;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
sinon.spy(subscriber, "stateChangeCallback");
|
||||
subscriber.reset();
|
||||
ps.subscribe(subscriber);
|
||||
info("subscribe the same listener twice to ensure it still doesn't call the callback");
|
||||
ps.subscribe(subscriber);
|
||||
ok(subscriber.stateChangeCallback.notCalled,
|
||||
"Check not called synchronously when subscribing");
|
||||
|
||||
let changePromise = subscriber.stateChangePromise;
|
||||
ps.setState({
|
||||
a: 1,
|
||||
});
|
||||
ok(subscriber.stateChangeCallback.notCalled,
|
||||
"Check not called synchronously for changes");
|
||||
let state = await changePromise;
|
||||
is(state, subscriber.stateChangeCallback.getCall(0).args[0],
|
||||
"Check resolved state is last state");
|
||||
is(JSON.stringify(state), `{"a":1}`, "Check callback state");
|
||||
|
||||
info("Testing consecutive setState");
|
||||
subscriber.reset();
|
||||
subscriber.stateChangeCallback.reset();
|
||||
changePromise = subscriber.stateChangePromise;
|
||||
ps.setState({
|
||||
a: 2,
|
||||
});
|
||||
ps.setState({
|
||||
a: 3,
|
||||
});
|
||||
ok(subscriber.stateChangeCallback.notCalled,
|
||||
"Check not called synchronously for changes");
|
||||
state = await changePromise;
|
||||
is(state, subscriber.stateChangeCallback.getCall(0).args[0],
|
||||
"Check resolved state is last state");
|
||||
is(JSON.stringify(subscriber.stateChangeCallback.getCall(0).args[0]), `{"a":3}`,
|
||||
"Check callback state matches second setState");
|
||||
|
||||
info("test unsubscribe");
|
||||
subscriber.stateChangeCallback = function unexpectedChange() {
|
||||
ok(false, "stateChangeCallback shouldn't be called after unsubscribing");
|
||||
};
|
||||
ps.unsubscribe(subscriber);
|
||||
ps.setState({
|
||||
a: 4,
|
||||
});
|
||||
await Promise.resolve("giving a chance for the callback to be called");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,111 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the accepted-cards element
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the accepted-cards element</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="sinon-7.2.7.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/accepted-cards.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<accepted-cards label="Accepted:"></accepted-cards>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the accepted-cards component **/
|
||||
|
||||
/* global sinon, PaymentDialogUtils */
|
||||
|
||||
import "../../res/components/accepted-cards.js";
|
||||
import {requestStore} from "../../res/mixins/PaymentStateSubscriberMixin.js";
|
||||
let emptyState = requestStore.getState();
|
||||
let acceptedElem = document.querySelector("accepted-cards");
|
||||
let allNetworks = PaymentDialogUtils.getCreditCardNetworks();
|
||||
|
||||
add_task(async function test_reConnected() {
|
||||
let itemsCount = acceptedElem.querySelectorAll(".accepted-cards-item").length;
|
||||
is(itemsCount, allNetworks.length, "Same number of items as there are supported networks");
|
||||
|
||||
let container = acceptedElem.parentNode;
|
||||
let removed = container.removeChild(acceptedElem);
|
||||
container.appendChild(removed);
|
||||
let newItemsCount = acceptedElem.querySelectorAll(".accepted-cards-item").length;
|
||||
is(itemsCount, newItemsCount, "Number of items doesnt changed when re-connected");
|
||||
});
|
||||
|
||||
add_task(async function test_someAccepted() {
|
||||
let supportedNetworks = ["discover", "amex"];
|
||||
let paymentMethods = [{
|
||||
supportedMethods: "basic-card",
|
||||
data: {
|
||||
supportedNetworks,
|
||||
},
|
||||
}];
|
||||
requestStore.setState({
|
||||
request: Object.assign({}, emptyState.request, {
|
||||
paymentMethods,
|
||||
}),
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
let showingItems = acceptedElem.querySelectorAll(".accepted-cards-item:not([hidden])");
|
||||
is(showingItems.length, 2,
|
||||
"Expected 2 items to be showing when 2 supportedNetworks are indicated");
|
||||
for (let network of allNetworks) {
|
||||
if (supportedNetworks.includes(network)) {
|
||||
ok(acceptedElem.querySelector(`[data-network-id='${network}']:not([hidden])`),
|
||||
`Item for the ${network} network expected to be visible`);
|
||||
} else {
|
||||
ok(acceptedElem.querySelector(`[data-network-id='${network}'][hidden]`),
|
||||
`Item for the ${network} network expected to be hidden`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
add_task(async function test_officialBranding() {
|
||||
// verify we get the expected result when isOfficialBranding returns true
|
||||
sinon.stub(PaymentDialogUtils, "isOfficialBranding").callsFake(() => { return true; });
|
||||
|
||||
let container = acceptedElem.parentNode;
|
||||
let removed = container.removeChild(acceptedElem);
|
||||
container.appendChild(removed);
|
||||
|
||||
ok(PaymentDialogUtils.isOfficialBranding.calledOnce,
|
||||
"isOfficialBranding was called");
|
||||
ok(acceptedElem.classList.contains("branded"),
|
||||
"The branded class is added when isOfficialBranding returns true");
|
||||
PaymentDialogUtils.isOfficialBranding.restore();
|
||||
|
||||
// verify we get the expected result when isOfficialBranding returns false
|
||||
sinon.stub(PaymentDialogUtils, "isOfficialBranding").callsFake(() => { return false; });
|
||||
|
||||
// the branded class is toggled in the 'connectedCallback',
|
||||
// so remove and re-add the element to re-evaluate branded-ness
|
||||
removed = container.removeChild(acceptedElem);
|
||||
container.appendChild(removed);
|
||||
|
||||
ok(PaymentDialogUtils.isOfficialBranding.calledOnce,
|
||||
"isOfficialBranding was called");
|
||||
ok(!acceptedElem.classList.contains("branded"),
|
||||
"The branded class is removed when isOfficialBranding returns false");
|
||||
PaymentDialogUtils.isOfficialBranding.restore();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,955 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the address-form element
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the address-form element</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="sinon-7.2.7.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
<script src="autofillEditForms.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="editDialog-shared.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/containers/address-form.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the address-form element **/
|
||||
|
||||
/* global sinon, PaymentDialogUtils */
|
||||
|
||||
import AddressForm from "../../res/containers/address-form.js";
|
||||
|
||||
let display = document.getElementById("display");
|
||||
|
||||
function checkAddressForm(customEl, expectedAddress) {
|
||||
const ADDRESS_PROPERTY_NAMES = [
|
||||
"given-name",
|
||||
"family-name",
|
||||
"organization",
|
||||
"street-address",
|
||||
"address-level2",
|
||||
"address-level1",
|
||||
"postal-code",
|
||||
"country",
|
||||
"email",
|
||||
"tel",
|
||||
];
|
||||
for (let propName of ADDRESS_PROPERTY_NAMES) {
|
||||
let expectedVal = expectedAddress[propName] || "";
|
||||
is(document.getElementById(propName).value,
|
||||
expectedVal.toString(),
|
||||
`Check ${propName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function sendStringAndCheckValidity(element, string, isValid) {
|
||||
fillField(element, string);
|
||||
ok(element.checkValidity() == isValid,
|
||||
`${element.id} should be ${isValid ? "valid" : "invalid"} ("${string}")`);
|
||||
}
|
||||
|
||||
add_task(async function test_initialState() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
|
||||
await form.requestStore.setState({
|
||||
"test-page": {},
|
||||
});
|
||||
|
||||
let {page} = form.requestStore.getState();
|
||||
is(page.id, "payment-summary", "Check initial page");
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
is(page.id, "payment-summary", "Check initial page after appending");
|
||||
|
||||
// :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline.
|
||||
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
|
||||
is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_pageTitle() {
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
// the element can have all the data attributes. We'll add them all up front
|
||||
let form = new AddressForm();
|
||||
let id = "shipping-address-page";
|
||||
form.id = id;
|
||||
form.dataset.titleAdd = `Add Title`;
|
||||
form.dataset.titleEdit = `Edit Title`;
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
|
||||
let newState = {
|
||||
page: { id },
|
||||
[id]: {},
|
||||
savedAddresses: {
|
||||
[address1.guid]: address1,
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {},
|
||||
paymentOptions: { shippingOption: "shipping" },
|
||||
},
|
||||
};
|
||||
await form.requestStore.setState(newState);
|
||||
await asyncElementRendered();
|
||||
is(form.pageTitleHeading.textContent, "Add Title", "Check 'add' title");
|
||||
|
||||
// test the 'edit' variation
|
||||
newState = deepClone(newState);
|
||||
newState[id].guid = address1.guid;
|
||||
await form.requestStore.setState(newState);
|
||||
await asyncElementRendered();
|
||||
is(form.pageTitleHeading.textContent, "Edit Title", "Check 'edit' title");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_backButton() {
|
||||
let form = new AddressForm();
|
||||
form.id = "test-page";
|
||||
form.dataset.titleAdd = "Sample add page title";
|
||||
form.dataset.backButtonLabel = "Back";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
|
||||
await form.requestStore.setState({
|
||||
"test-page": {},
|
||||
page: {
|
||||
id: "test-page",
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {},
|
||||
paymentOptions: {},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
let stateChangePromise = promiseStateChange(form.requestStore);
|
||||
is(form.pageTitleHeading.textContent, "Sample add page title", "Check title");
|
||||
|
||||
is(form.backButton.textContent, "Back", "Check label");
|
||||
form.backButton.scrollIntoView();
|
||||
synthesizeMouseAtCenter(form.backButton, {});
|
||||
|
||||
let {page} = await stateChangePromise;
|
||||
is(page.id, "payment-summary", "Check initial page after appending");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_saveButton() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
form.dataset.nextButtonLabel = "Next";
|
||||
form.dataset.errorGenericSave = "Generic error";
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
form.requestStore.setState({
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(form.saveButton.disabled, "Save button initially disabled");
|
||||
fillField(form.form.querySelector("#given-name"), "Jaws");
|
||||
fillField(form.form.querySelector("#family-name"), "Swaj");
|
||||
fillField(form.form.querySelector("#organization"), "Allizom");
|
||||
fillField(form.form.querySelector("#street-address"), "404 Internet Super Highway");
|
||||
fillField(form.form.querySelector("#address-level2"), "Firefoxity City");
|
||||
fillField(form.form.querySelector("#country"), "US");
|
||||
fillField(form.form.querySelector("#address-level1"), "CA");
|
||||
fillField(form.form.querySelector("#postal-code"), "00001");
|
||||
fillField(form.form.querySelector("#tel"), "+15555551212");
|
||||
|
||||
ok(!form.saveButton.disabled, "Save button is enabled after filling");
|
||||
|
||||
info("blanking the street-address");
|
||||
fillField(form.form.querySelector("#street-address"), "");
|
||||
ok(form.saveButton.disabled, "Save button is disabled after blanking street-address");
|
||||
form.form.querySelector("#street-address").blur();
|
||||
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
|
||||
is(fieldsVisiblyInvalid.length, 1, "Check 1 field visibly invalid after blanking and blur");
|
||||
is(fieldsVisiblyInvalid[0].id, "street-address", "Check #street-address is visibly invalid");
|
||||
|
||||
fillField(form.form.querySelector("#street-address"), "404 Internet Super Highway");
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0, "Check no fields visibly invalid");
|
||||
ok(!form.saveButton.disabled, "Save button is enabled after re-filling street-address");
|
||||
|
||||
fillField(form.form.querySelector("#country"), "CA");
|
||||
ok(form.saveButton.disabled, "Save button is disabled after changing the country to Canada");
|
||||
fillField(form.form.querySelector("#country"), "US");
|
||||
ok(form.saveButton.disabled,
|
||||
"Save button is disabled after changing the country back to US since address-level1 " +
|
||||
"got cleared when changing countries");
|
||||
fillField(form.form.querySelector("#address-level1"), "CA");
|
||||
ok(!form.saveButton.disabled, "Save button is enabled after re-entering address-level1");
|
||||
|
||||
let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
|
||||
is(form.saveButton.textContent, "Next", "Check label");
|
||||
form.saveButton.scrollIntoView();
|
||||
synthesizeMouseAtCenter(form.saveButton, {});
|
||||
|
||||
let details = await messagePromise;
|
||||
ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
|
||||
delete details.messageID;
|
||||
is(details.collectionName, "addresses", "Check collectionName");
|
||||
isDeeply(details, {
|
||||
collectionName: "addresses",
|
||||
guid: undefined,
|
||||
messageType: "updateAutofillRecord",
|
||||
record: {
|
||||
"given-name": "Jaws",
|
||||
"family-name": "Swaj",
|
||||
"additional-name": "",
|
||||
"organization": "Allizom",
|
||||
"street-address": "404 Internet Super Highway",
|
||||
"address-level3": "",
|
||||
"address-level2": "Firefoxity City",
|
||||
"address-level1": "CA",
|
||||
"postal-code": "00001",
|
||||
"country": "US",
|
||||
"tel": "+15555551212",
|
||||
},
|
||||
}, "Check event details for the message to chrome");
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_genericError() {
|
||||
let form = new AddressForm();
|
||||
form.id = "test-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "test-page",
|
||||
error: "Generic Error",
|
||||
},
|
||||
});
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(!isHidden(form.genericErrorText), "Error message should be visible");
|
||||
is(form.genericErrorText.textContent, "Generic Error", "Check error message");
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_edit() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.dataset.updateButtonLabel = "Update";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
guid: address1.guid,
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check no fields are visibly invalid on an 'edit' form with a complete address");
|
||||
checkAddressForm(form, address1);
|
||||
|
||||
ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid address");
|
||||
|
||||
info("test change to minimal record");
|
||||
let minimalAddress = {
|
||||
"given-name": address1["given-name"],
|
||||
guid: "9gnjdhen46",
|
||||
};
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
guid: minimalAddress.guid,
|
||||
},
|
||||
savedAddresses: {
|
||||
[minimalAddress.guid]: deepClone(minimalAddress),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
is(form.saveButton.textContent, "Update", "Check label");
|
||||
checkAddressForm(form, minimalAddress);
|
||||
ok(form.saveButton.disabled, "Save button should be disabled if only the name is filled");
|
||||
ok(form.querySelectorAll(":-moz-ui-invalid").length > 3,
|
||||
"Check fields are visibly invalid on an 'edit' form with only the given-name filled");
|
||||
is(form.querySelectorAll("#country:-moz-ui-invalid").length, 1,
|
||||
"Check that the country `select` is marked as invalid");
|
||||
|
||||
info("change to no selected address");
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check no fields are visibly invalid on an empty 'add' form after being an edit form");
|
||||
checkAddressForm(form, {
|
||||
country: "US",
|
||||
});
|
||||
ok(form.saveButton.disabled, "Save button should be disabled for an empty form");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_restricted_address_fields() {
|
||||
let form = new AddressForm();
|
||||
form.id = "payer-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedPayerAddress");
|
||||
form.dataset.errorGenericSave = "Generic error";
|
||||
form.dataset.fieldRequiredSymbol = "*";
|
||||
form.dataset.nextButtonLabel = "Next";
|
||||
await form.promiseReady;
|
||||
form.form.dataset.extraRequiredFields = "name email tel";
|
||||
display.appendChild(form);
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "payer-address-page",
|
||||
},
|
||||
"payer-address-page": {
|
||||
addressFields: "name email tel",
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
|
||||
|
||||
ok(!isHidden(form.form.querySelector("#given-name")),
|
||||
"given-name should be visible");
|
||||
ok(!isHidden(form.form.querySelector("#additional-name")),
|
||||
"additional-name should be visible");
|
||||
ok(!isHidden(form.form.querySelector("#family-name")),
|
||||
"family-name should be visible");
|
||||
ok(isHidden(form.form.querySelector("#organization")),
|
||||
"organization should be hidden");
|
||||
ok(isHidden(form.form.querySelector("#street-address")),
|
||||
"street-address should be hidden");
|
||||
ok(isHidden(form.form.querySelector("#address-level2")),
|
||||
"address-level2 should be hidden");
|
||||
ok(isHidden(form.form.querySelector("#address-level1")),
|
||||
"address-level1 should be hidden");
|
||||
ok(isHidden(form.form.querySelector("#postal-code")),
|
||||
"postal-code should be hidden");
|
||||
ok(isHidden(form.form.querySelector("#country")),
|
||||
"country should be hidden");
|
||||
ok(!isHidden(form.form.querySelector("#email")),
|
||||
"email should be visible");
|
||||
let telField = form.form.querySelector("#tel");
|
||||
ok(!isHidden(telField),
|
||||
"tel should be visible");
|
||||
let telContainer = telField.closest(`#${telField.id}-container`);
|
||||
ok(telContainer.hasAttribute("required"), "tel container should have required attribute");
|
||||
let telSpan = telContainer.querySelector("span");
|
||||
is(telSpan.getAttribute("fieldRequiredSymbol"), "*",
|
||||
"tel span should have asterisk as fieldRequiredSymbol");
|
||||
is(getComputedStyle(telSpan, "::after").content, "attr(fieldRequiredSymbol)",
|
||||
"Asterisk should be on tel");
|
||||
|
||||
fillField(form.form.querySelector("#given-name"), "John");
|
||||
fillField(form.form.querySelector("#family-name"), "Smith");
|
||||
ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
|
||||
fillField(form.form.querySelector("#email"), "john@example.com");
|
||||
ok(form.saveButton.disabled,
|
||||
"Save button should be disabled due to empty fields");
|
||||
fillField(form.form.querySelector("#tel"), "+15555555555");
|
||||
ok(!form.saveButton.disabled, "Save button should be enabled with all required fields filled");
|
||||
|
||||
form.remove();
|
||||
await form.requestStore.setState({
|
||||
"payer-address-page": {},
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_field_validation() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
form.dataset.fieldRequiredSymbol = "*";
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
let postalCodeInput = form.form.querySelector("#postal-code");
|
||||
let addressLevel1Input = form.form.querySelector("#address-level1");
|
||||
ok(!postalCodeInput.value, "postal-code should be empty by default");
|
||||
ok(!addressLevel1Input.value, "address-level1 should be empty by default");
|
||||
ok(!postalCodeInput.checkValidity(), "postal-code should be invalid by default");
|
||||
ok(!addressLevel1Input.checkValidity(), "address-level1 should be invalid by default");
|
||||
|
||||
let countrySelect = form.form.querySelector("#country");
|
||||
let requiredFields = [
|
||||
form.form.querySelector("#given-name"),
|
||||
form.form.querySelector("#street-address"),
|
||||
form.form.querySelector("#address-level2"),
|
||||
postalCodeInput,
|
||||
addressLevel1Input,
|
||||
countrySelect,
|
||||
];
|
||||
for (let field of requiredFields) {
|
||||
let container = field.closest(`#${field.id}-container`);
|
||||
ok(container.hasAttribute("required"), `#${field.id} container should have required attribute`);
|
||||
let span = container.querySelector("span");
|
||||
is(span.getAttribute("fieldRequiredSymbol"), "*",
|
||||
"span should have asterisk as fieldRequiredSymbol");
|
||||
is(getComputedStyle(span, "::after").content, "attr(fieldRequiredSymbol)",
|
||||
"Asterisk should be on " + field.id);
|
||||
}
|
||||
|
||||
ok(form.saveButton.disabled, "Save button should be disabled upon load");
|
||||
|
||||
fillField(countrySelect, "US");
|
||||
|
||||
sendStringAndCheckValidity(addressLevel1Input, "MI", true);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "B4N4N4", false);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "NS", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", false);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "11109", true);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "NS", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "06390-0001", true);
|
||||
|
||||
fillField(countrySelect, "CA");
|
||||
|
||||
sendStringAndCheckValidity(postalCodeInput, "00001", false);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "CA", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "94043", false);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "B4N4N4", true);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "MI", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "R3J 3C7", true);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "", false);
|
||||
sendStringAndCheckValidity(postalCodeInput, "11109", false);
|
||||
sendStringAndCheckValidity(addressLevel1Input, "NS", true);
|
||||
sendStringAndCheckValidity(postalCodeInput, "06390-0001", false);
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_merchantShippingAddressErrors() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.promiseReady;
|
||||
|
||||
// Merchant errors only make sense when editing a record so add one.
|
||||
let address1 = deepClone(PTU.Addresses.TimBR);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
const state = {
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
guid: address1.guid,
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {
|
||||
shippingAddressErrors: {
|
||||
addressLine: "Street address needs to start with a D",
|
||||
city: "City needs to start with a B",
|
||||
country: "Country needs to start with a C",
|
||||
dependentLocality: "Can only be SUBURBS, not NEIGHBORHOODS",
|
||||
organization: "organization needs to start with an A",
|
||||
phone: "Telephone needs to start with a 9",
|
||||
postalCode: "Postal code needs to start with a 0",
|
||||
recipient: "Name needs to start with a Z",
|
||||
region: "Region needs to start with a Y",
|
||||
regionCode: "Regions must be 1 to 3 characters in length (sometimes ;) )",
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
};
|
||||
display.appendChild(form);
|
||||
await form.requestStore.setState(state);
|
||||
await asyncElementRendered();
|
||||
|
||||
function checkValidationMessage(selector, property) {
|
||||
let expected = state.request.paymentDetails.shippingAddressErrors[property];
|
||||
let container = form.form.querySelector(selector + "-container");
|
||||
ok(!isHidden(container), selector + "-container should be visible");
|
||||
is(form.form.querySelector(selector).validationMessage,
|
||||
expected,
|
||||
"Validation message should match for " + selector);
|
||||
}
|
||||
|
||||
ok(form.saveButton.disabled, "Save button should be disabled due to validation errors");
|
||||
|
||||
checkValidationMessage("#street-address", "addressLine");
|
||||
checkValidationMessage("#address-level2", "city");
|
||||
checkValidationMessage("#address-level3", "dependentLocality");
|
||||
checkValidationMessage("#country", "country");
|
||||
checkValidationMessage("#organization", "organization");
|
||||
checkValidationMessage("#tel", "phone");
|
||||
checkValidationMessage("#postal-code", "postalCode");
|
||||
checkValidationMessage("#given-name", "recipient");
|
||||
checkValidationMessage("#address-level1", "regionCode");
|
||||
isnot(form.form.querySelector("#address-level1"),
|
||||
state.request.paymentDetails.shippingAddressErrors.region,
|
||||
"When both region and regionCode are supplied we only show the 'regionCode' error");
|
||||
|
||||
// TODO: bug 1482808 - the save button should be enabled after editing the fields
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_customMerchantValidity_reset() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.promiseReady;
|
||||
|
||||
// Merchant errors only make sense when editing a record so add one.
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
const state = {
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
guid: address1.guid,
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {
|
||||
shippingAddressErrors: {
|
||||
addressLine: "Street address needs to start with a D",
|
||||
city: "City needs to start with a B",
|
||||
country: "Country needs to start with a C",
|
||||
organization: "organization needs to start with an A",
|
||||
phone: "Telephone needs to start with a 9",
|
||||
postalCode: "Postal code needs to start with a 0",
|
||||
recipient: "Name needs to start with a Z",
|
||||
region: "Region needs to start with a Y",
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
};
|
||||
await form.requestStore.setState(state);
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(form.querySelectorAll(":-moz-ui-invalid").length > 0, "Check fields are visibly invalid");
|
||||
info("merchant cleared the errors");
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentDetails: {
|
||||
shippingAddressErrors: {},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check fields are visibly valid - custom validity cleared");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_customMerchantValidity_shippingAddressForm() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.promiseReady;
|
||||
|
||||
// Merchant errors only make sense when editing a record so add one.
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
const state = {
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
guid: address1.guid,
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {
|
||||
billingAddressErrors: {
|
||||
addressLine: "Billing Street address needs to start with a D",
|
||||
city: "Billing City needs to start with a B",
|
||||
country: "Billing Country needs to start with a C",
|
||||
organization: "Billing organization needs to start with an A",
|
||||
phone: "Billing Telephone needs to start with a 9",
|
||||
postalCode: "Billing Postal code needs to start with a 0",
|
||||
recipient: "Billing Name needs to start with a Z",
|
||||
region: "Billing Region needs to start with a Y",
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
};
|
||||
await form.requestStore.setState(state);
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check fields are visibly valid - billing errors are not relevant to a shipping address form");
|
||||
|
||||
// now switch in some shipping address errors
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentDetails: {
|
||||
shippingAddressErrors: {
|
||||
addressLine: "Street address needs to start with a D",
|
||||
city: "City needs to start with a B",
|
||||
country: "Country needs to start with a C",
|
||||
organization: "organization needs to start with an A",
|
||||
phone: "Telephone needs to start with a 9",
|
||||
postalCode: "Postal code needs to start with a 0",
|
||||
recipient: "Name needs to start with a Z",
|
||||
region: "Region needs to start with a Y",
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(form.querySelectorAll(":-moz-ui-invalid").length >= 8, "Check fields are visibly invalid");
|
||||
});
|
||||
|
||||
add_task(async function test_customMerchantValidity_billingAddressForm() {
|
||||
let form = new AddressForm();
|
||||
form.id = "billing-address-page";
|
||||
form.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID");
|
||||
await form.promiseReady;
|
||||
|
||||
// Merchant errors only make sense when editing a record so add one.
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
const state = {
|
||||
page: {
|
||||
id: "billing-address-page",
|
||||
},
|
||||
"billing-address-page": {
|
||||
guid: address1.guid,
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {
|
||||
shippingAddressErrors: {
|
||||
addressLine: "Street address needs to start with a D",
|
||||
city: "City needs to start with a B",
|
||||
country: "Country needs to start with a C",
|
||||
organization: "organization needs to start with an A",
|
||||
phone: "Telephone needs to start with a 9",
|
||||
postalCode: "Postal code needs to start with a 0",
|
||||
recipient: "Name needs to start with a Z",
|
||||
region: "Region needs to start with a Y",
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
};
|
||||
await form.requestStore.setState(state);
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check fields are visibly valid - shipping errors are not relevant to a billing address form");
|
||||
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentDetails: {
|
||||
paymentMethodErrors: {
|
||||
billingAddress: {
|
||||
addressLine: "Billing Street address needs to start with a D",
|
||||
city: "Billing City needs to start with a B",
|
||||
country: "Billing Country needs to start with a C",
|
||||
organization: "Billing organization needs to start with an A",
|
||||
phone: "Billing Telephone needs to start with a 9",
|
||||
postalCode: "Billing Postal code needs to start with a 0",
|
||||
recipient: "Billing Name needs to start with a Z",
|
||||
region: "Billing Region needs to start with a Y",
|
||||
},
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
ok(form.querySelectorAll(":-moz-ui-invalid").length >= 8,
|
||||
"Check billing fields are visibly invalid");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_merchantPayerAddressErrors() {
|
||||
let form = new AddressForm();
|
||||
form.id = "payer-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedPayerAddress");
|
||||
|
||||
await form.promiseReady;
|
||||
form.form.dataset.extraRequiredFields = "name email tel";
|
||||
|
||||
// Merchant errors only make sense when editing a record so add one.
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
const state = {
|
||||
page: {
|
||||
id: "payer-address-page",
|
||||
},
|
||||
"payer-address-page": {
|
||||
addressFields: "name email tel",
|
||||
guid: address1.guid,
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {
|
||||
payerErrors: {
|
||||
email: "Email must be @mozilla.org",
|
||||
name: "Name needs to start with a W",
|
||||
phone: "Telephone needs to start with a 1",
|
||||
},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
};
|
||||
await form.requestStore.setState(state);
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
function checkValidationMessage(selector, property) {
|
||||
is(form.form.querySelector(selector).validationMessage,
|
||||
state.request.paymentDetails.payerErrors[property],
|
||||
"Validation message should match for " + selector);
|
||||
}
|
||||
|
||||
ok(form.saveButton.disabled, "Save button should be disabled due to validation errors");
|
||||
|
||||
checkValidationMessage("#tel", "phone");
|
||||
checkValidationMessage("#family-name", "name");
|
||||
checkValidationMessage("#email", "email");
|
||||
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 3, "Check payer fields are visibly invalid");
|
||||
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentDetails: {
|
||||
payerErrors: {},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check payer fields are visibly valid after clearing merchant errors");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_field_validation() {
|
||||
let getFormFormatStub = sinon.stub(PaymentDialogUtils, "getFormFormat");
|
||||
getFormFormatStub.returns({
|
||||
addressLevel1Label: "state",
|
||||
postalCodeLabel: "US",
|
||||
fieldsOrder: [
|
||||
{fieldId: "name", newLine: true},
|
||||
{fieldId: "organization", newLine: true},
|
||||
{fieldId: "street-address", newLine: true},
|
||||
{fieldId: "address-level2"},
|
||||
],
|
||||
});
|
||||
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.promiseReady;
|
||||
const state = {
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
},
|
||||
request: {
|
||||
paymentDetails: {
|
||||
shippingAddressErrors: {},
|
||||
},
|
||||
paymentOptions: {},
|
||||
},
|
||||
};
|
||||
await form.requestStore.setState(state);
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(form.saveButton.disabled, "Save button should be disabled due to empty fields");
|
||||
|
||||
let postalCodeInput = form.form.querySelector("#postal-code");
|
||||
let addressLevel1Input = form.form.querySelector("#address-level1");
|
||||
ok(!postalCodeInput.value, "postal-code should be empty by default");
|
||||
ok(!addressLevel1Input.value, "address-level1 should be empty by default");
|
||||
ok(postalCodeInput.checkValidity(),
|
||||
"postal-code should be valid by default when it is not visible");
|
||||
ok(addressLevel1Input.checkValidity(),
|
||||
"address-level1 should be valid by default when it is not visible");
|
||||
|
||||
getFormFormatStub.restore();
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_field_validation_dom_popup() {
|
||||
let form = new AddressForm();
|
||||
form.id = "shipping-address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
await form.promiseReady;
|
||||
const state = {
|
||||
page: {
|
||||
id: "shipping-address-page",
|
||||
},
|
||||
"shipping-address-page": {
|
||||
},
|
||||
};
|
||||
|
||||
await form.requestStore.setState(state);
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
const BAD_POSTAL_CODE = "hi mom";
|
||||
let postalCode = form.querySelector("#postal-code");
|
||||
postalCode.focus();
|
||||
sendString(BAD_POSTAL_CODE, window);
|
||||
postalCode.blur();
|
||||
let errorTextSpan = postalCode.parentNode.querySelector(".error-text");
|
||||
is(errorTextSpan.textContent, "Please match the requested format.",
|
||||
"DOM validation messages should be reflected in the error-text #1");
|
||||
|
||||
postalCode.focus();
|
||||
while (postalCode.value) {
|
||||
sendKey("BACK_SPACE", window);
|
||||
}
|
||||
postalCode.blur();
|
||||
is(errorTextSpan.textContent, "Please fill out this field.",
|
||||
"DOM validation messages should be reflected in the error-text #2");
|
||||
|
||||
postalCode.focus();
|
||||
sendString("12345", window);
|
||||
is(errorTextSpan.innerText, "", "DOM validation message should be removed when no error");
|
||||
postalCode.blur();
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_hiddenMailingAddressFieldsCleared() {
|
||||
let form = new AddressForm();
|
||||
form.id = "address-page";
|
||||
form.setAttribute("selected-state-key", "selectedShippingAddress");
|
||||
form.dataset.updateButtonLabel = "Update";
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
let address1 = deepClone(PTU.Addresses.TimBL);
|
||||
address1.guid = "9864798564";
|
||||
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "address-page",
|
||||
},
|
||||
"address-page": {
|
||||
guid: address1.guid,
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
info("Change the country to hide address-level1");
|
||||
fillField(form.form.querySelector("#country"), "DE");
|
||||
|
||||
let expectedRecord = Object.assign({}, address1, {
|
||||
country: "DE",
|
||||
// address-level1 & 3 aren't used for Germany so should be blanked.
|
||||
"address-level1": "",
|
||||
"address-level3": "",
|
||||
});
|
||||
delete expectedRecord.guid;
|
||||
// The following were not shown so shouldn't be part of the message:
|
||||
delete expectedRecord.email;
|
||||
|
||||
let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
|
||||
form.saveButton.scrollIntoView();
|
||||
synthesizeMouseAtCenter(form.saveButton, {});
|
||||
|
||||
info("Waiting for messagePromise");
|
||||
let details = await messagePromise;
|
||||
info("/Waiting for messagePromise");
|
||||
delete details.messageID;
|
||||
is(details.collectionName, "addresses", "Check collectionName");
|
||||
isDeeply(details, {
|
||||
collectionName: "addresses",
|
||||
guid: address1.guid,
|
||||
messageType: "updateAutofillRecord",
|
||||
record: expectedRecord,
|
||||
}, "Check update event details for the message to chrome");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,177 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the address-option component
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the address-option component</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
<script src="autofillEditForms.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<option id="option1"
|
||||
data-field-separator=", "
|
||||
address-level1="MI"
|
||||
address-level2="Some City"
|
||||
country="US"
|
||||
email="foo@bar.com"
|
||||
name="John Smith"
|
||||
postal-code="90210"
|
||||
street-address="123 Sesame Street,
Apt 40"
|
||||
tel="+1 519 555-5555"
|
||||
value="option1"
|
||||
guid="option1"></option>
|
||||
<option id="option2"
|
||||
data-field-separator=", "
|
||||
value="option2"
|
||||
guid="option2"></option>
|
||||
|
||||
<rich-select id="richSelect1"
|
||||
option-type="address-option"></rich-select>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the address-option component **/
|
||||
|
||||
import "../../res/components/address-option.js";
|
||||
import "../../res/components/rich-select.js";
|
||||
|
||||
let option1 = document.getElementById("option1");
|
||||
let option2 = document.getElementById("option2");
|
||||
let richSelect1 = document.getElementById("richSelect1");
|
||||
|
||||
add_task(async function test_populated_option_rendering() {
|
||||
richSelect1.popupBox.appendChild(option1);
|
||||
richSelect1.value = option1.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
|
||||
is(richOption.name, "John Smith", "Check name getter");
|
||||
is(richOption.streetAddress, "123 Sesame Street,\nApt 40", "Check streetAddress getter");
|
||||
is(richOption.addressLevel2, "Some City", "Check addressLevel2 getter");
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma");
|
||||
ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma");
|
||||
is(richOption._line1.innerText, "John Smith, 123 Sesame Street, Apt 40", "Line 1 text");
|
||||
is(richOption._line2.innerText, "Some City, MI, 90210, US", "Line 2 text");
|
||||
|
||||
// Note that innerText takes visibility into account so that's why it's used over textContent here
|
||||
is(richOption._name.innerText, "John Smith", "name text");
|
||||
is(richOption["_street-address"].innerText, "123 Sesame Street, Apt 40", "street-address text");
|
||||
is(richOption["_address-level2"].innerText, "Some City", "address-level2 text");
|
||||
|
||||
is(richOption._email.parentElement, null,
|
||||
"Check email field isn't in the document for a mailing-address option");
|
||||
});
|
||||
|
||||
// Same option as the last test but with @break-after-nth-field=1
|
||||
add_task(async function test_breakAfterNthField() {
|
||||
richSelect1.popupBox.appendChild(option1);
|
||||
richSelect1.value = option1.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
richOption.breakAfterNthField = 1;
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma");
|
||||
ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma");
|
||||
is(richOption._line1.innerText, "John Smith", "Line 1 text with breakAfterNthField = 1");
|
||||
is(richOption._line2.innerText, "123 Sesame Street, Apt 40, Some City, MI, 90210, US",
|
||||
"Line 2 text with breakAfterNthField = 1");
|
||||
});
|
||||
|
||||
add_task(async function test_addressField_mailingAddress() {
|
||||
richSelect1.popupBox.appendChild(option1);
|
||||
richSelect1.value = option1.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
richOption.addressFields = "mailing-address";
|
||||
await asyncElementRendered();
|
||||
is(richOption.getAttribute("address-fields"), "mailing-address", "Check @address-fields");
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma");
|
||||
ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma");
|
||||
is(richOption._line1.innerText, "John Smith, 123 Sesame Street, Apt 40", "Line 1 text");
|
||||
is(richOption._line2.innerText, "Some City, MI, 90210, US", "Line 2 text");
|
||||
|
||||
ok(!isHidden(richOption._line2), "Line 2 should be visible when it's used");
|
||||
|
||||
is(richOption._email.parentElement, null,
|
||||
"Check email field isn't in the document for a mailing-address option");
|
||||
});
|
||||
|
||||
add_task(async function test_addressField_nameEmail() {
|
||||
richSelect1.popupBox.appendChild(option1);
|
||||
richSelect1.value = option1.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
richOption.addressFields = "name email";
|
||||
await asyncElementRendered();
|
||||
is(richOption.getAttribute("address-fields"), "name email", "Check @address-fields");
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
ok(!richOption._line1.innerText.trim().endsWith(","), "Line 1 should not end with a comma");
|
||||
ok(!richOption._line2.innerText.trim().endsWith(","), "Line 2 should not end with a comma");
|
||||
is(richOption._line1.innerText, "John Smith, foo@bar.com", "Line 1 text");
|
||||
is(richOption._line2.innerText, "", "Line 2 text");
|
||||
|
||||
ok(isHidden(richOption._line2), "Line 2 should be hidden when it's not used");
|
||||
|
||||
isnot(richOption._email.parentElement, null,
|
||||
"Check email field is in the document for a 'name email' option");
|
||||
});
|
||||
|
||||
add_task(async function test_missing_fields_option_rendering() {
|
||||
richSelect1.popupBox.appendChild(option2);
|
||||
richSelect1.value = option2.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
is(richOption.name, null, "Check name getter");
|
||||
is(richOption.streetAddress, null, "Check streetAddress getter");
|
||||
is(richOption.addressLevel2, null, "Check addressLevel2 getter");
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
is(richOption._name.innerText, "", "name text");
|
||||
is(window.getComputedStyle(richOption._name, "::before").content, "attr(data-missing-string)",
|
||||
"Check missing field pseudo content");
|
||||
is(richOption._name.getAttribute("data-missing-string"), "Name Missing",
|
||||
"Check @data-missing-string");
|
||||
is(richOption._email.parentElement, null,
|
||||
"Check email field isn't in the document for a mailing-address option");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,278 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the address-picker component
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the address-picker component</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
<script src="autofillEditForms.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<address-picker id="picker1"
|
||||
data-field-separator=", "
|
||||
data-invalid-label="Picker1: Missing or Invalid"
|
||||
selected-state-key="selectedShippingAddress"></address-picker>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the address-picker component **/
|
||||
|
||||
import "../../res/containers/address-picker.js";
|
||||
|
||||
let picker1 = document.getElementById("picker1");
|
||||
|
||||
add_task(async function test_empty() {
|
||||
ok(picker1, "Check picker1 exists");
|
||||
let {savedAddresses} = picker1.requestStore.getState();
|
||||
is(Object.keys(savedAddresses).length, 0, "Check empty initial state");
|
||||
is(picker1.editLink.hidden, true, "Check that picker edit link is hidden");
|
||||
is(picker1.dropdown.popupBox.children.length, 0, "Check dropdown is empty");
|
||||
});
|
||||
|
||||
add_task(async function test_initialSet() {
|
||||
picker1.requestStore.setState({
|
||||
savedAddresses: {
|
||||
"48bnds6854t": {
|
||||
"address-level1": "MI",
|
||||
"address-level2": "Some City",
|
||||
"country": "US",
|
||||
"guid": "48bnds6854t",
|
||||
"name": "Mr. Foo",
|
||||
"postal-code": "90210",
|
||||
"street-address": "123 Sesame Street,\nApt 40",
|
||||
"tel": "+1 519 555-5555",
|
||||
timeLastUsed: 200,
|
||||
},
|
||||
"68gjdh354j": {
|
||||
"address-level1": "CA",
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "68gjdh354j",
|
||||
"name": "Mrs. Bar",
|
||||
"postal-code": "94041",
|
||||
"street-address": "P.O. Box 123",
|
||||
"tel": "+1 650 555-5555",
|
||||
timeLastUsed: 300,
|
||||
},
|
||||
"abcde12345": {
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "abcde12345",
|
||||
"name": "Mrs. Fields",
|
||||
timeLastUsed: 100,
|
||||
},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
let options = picker1.dropdown.popupBox.children;
|
||||
is(options.length, 3, "Check dropdown has all addresses");
|
||||
ok(options[0].textContent.includes("Mrs. Bar"), "Check first address based on timeLastUsed");
|
||||
ok(options[1].textContent.includes("Mr. Foo"), "Check second address based on timeLastUsed");
|
||||
ok(options[2].textContent.includes("Mrs. Fields"), "Check third address based on timeLastUsed");
|
||||
});
|
||||
|
||||
add_task(async function test_update() {
|
||||
picker1.requestStore.setState({
|
||||
savedAddresses: {
|
||||
"48bnds6854t": {
|
||||
// Same GUID, different values to trigger an update
|
||||
"address-level1": "MI-edit",
|
||||
// address-level2 was cleared which means it's not returned
|
||||
"country": "CA",
|
||||
"guid": "48bnds6854t",
|
||||
"name": "Mr. Foo-edit",
|
||||
"postal-code": "90210-1234",
|
||||
"street-address": "new-edit",
|
||||
"tel": "+1 650 555-5555",
|
||||
},
|
||||
"68gjdh354j": {
|
||||
"address-level1": "CA",
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "68gjdh354j",
|
||||
"name": "Mrs. Bar",
|
||||
"postal-code": "94041",
|
||||
"street-address": "P.O. Box 123",
|
||||
"tel": "+1 650 555-5555",
|
||||
},
|
||||
"abcde12345": {
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "abcde12345",
|
||||
"name": "Mrs. Fields",
|
||||
},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
let options = picker1.dropdown.popupBox.children;
|
||||
is(options.length, 3, "Check dropdown still has all addresses");
|
||||
ok(options[0].textContent.includes("Mr. Foo-edit"), "Check updated name in first address");
|
||||
ok(!options[0].getAttribute("address-level2"), "Check removed first address-level2");
|
||||
ok(options[1].textContent.includes("Mrs. Bar"), "Check that name is the same in second address");
|
||||
ok(options[1].getAttribute("street-address").includes("P.O. Box 123"),
|
||||
"Check second address is the same");
|
||||
ok(options[2].textContent.includes("Mrs. Fields"),
|
||||
"Check that name is the same in third address");
|
||||
is(options[2].getAttribute("street-address"), null, "Check third address is missing");
|
||||
});
|
||||
|
||||
add_task(async function test_change_selected_address() {
|
||||
let options = picker1.dropdown.popupBox.children;
|
||||
is(picker1.dropdown.selectedOption, null, "Should default to no selected option");
|
||||
is(picker1.editLink.hidden, true, "Picker edit link should be hidden when no option is selected");
|
||||
let {selectedShippingAddress} = picker1.requestStore.getState();
|
||||
is(selectedShippingAddress, null, "store should have no option selected");
|
||||
ok(!picker1.classList.contains("invalid-selected-option"), "No validation on an empty selection");
|
||||
ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
|
||||
|
||||
picker1.dropdown.popupBox.focus();
|
||||
synthesizeKey(options[2].getAttribute("name"), {});
|
||||
await asyncElementRendered();
|
||||
|
||||
let selectedOption = picker1.dropdown.selectedOption;
|
||||
is(selectedOption, options[2], "Selected option should now be the third option");
|
||||
selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
|
||||
is(selectedShippingAddress, selectedOption.getAttribute("guid"),
|
||||
"store should have third option selected");
|
||||
// The third option is missing some fields. Make sure that it is marked as such.
|
||||
ok(picker1.classList.contains("invalid-selected-option"), "The third option is missing fields");
|
||||
ok(!isHidden(picker1.invalidLabel), "The invalid label should be visible");
|
||||
is(picker1.invalidLabel.innerText, picker1.dataset.invalidLabel, "Check displayed error text");
|
||||
|
||||
picker1.dropdown.popupBox.focus();
|
||||
synthesizeKey(options[1].getAttribute("name"), {});
|
||||
await asyncElementRendered();
|
||||
|
||||
selectedOption = picker1.dropdown.selectedOption;
|
||||
is(selectedOption, options[1], "Selected option should now be the second option");
|
||||
selectedShippingAddress = picker1.requestStore.getState().selectedShippingAddress;
|
||||
is(selectedShippingAddress, selectedOption.getAttribute("guid"),
|
||||
"store should have second option selected");
|
||||
ok(!picker1.classList.contains("invalid-selected-option"), "The second option has all fields");
|
||||
ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
|
||||
});
|
||||
|
||||
add_task(async function test_address_combines_name_street_level2_level1_postalCode_country() {
|
||||
let options = picker1.dropdown.popupBox.children;
|
||||
let richoption1 = picker1.dropdown.querySelector(".rich-select-selected-option");
|
||||
/* eslint-disable max-len */
|
||||
is(richoption1.innerText,
|
||||
`${options[1].getAttribute("name")}, ${options[1].getAttribute("street-address")}
|
||||
${options[1].getAttribute("address-level2")}, ${options[1].getAttribute("address-level1")}, ${options[1].getAttribute("postal-code")}, ${options[1].getAttribute("country")}`,
|
||||
"The address shown should be human readable and include all fields");
|
||||
/* eslint-enable max-len */
|
||||
|
||||
picker1.dropdown.popupBox.focus();
|
||||
synthesizeKey(options[2].getAttribute("name"), {});
|
||||
await asyncElementRendered();
|
||||
|
||||
richoption1 = picker1.dropdown.querySelector(".rich-select-selected-option");
|
||||
// "Missing …" text is rendered via a pseudo element content and isn't included in innerText
|
||||
is(richoption1.innerText, "Mrs. Fields, \nMountain View, , US",
|
||||
"The address shown should be human readable and include all fields");
|
||||
|
||||
picker1.dropdown.popupBox.focus();
|
||||
synthesizeKey(options[1].getAttribute("name"), {});
|
||||
await asyncElementRendered();
|
||||
});
|
||||
|
||||
add_task(async function test_delete() {
|
||||
picker1.requestStore.setState({
|
||||
savedAddresses: {
|
||||
// 48bnds6854t and abcde12345 was deleted
|
||||
"68gjdh354j": {
|
||||
"address-level1": "CA",
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "68gjdh354j",
|
||||
"name": "Mrs. Bar",
|
||||
"postal-code": "94041",
|
||||
"street-address": "P.O. Box 123",
|
||||
"tel": "+1 650 555-5555",
|
||||
},
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
let options = picker1.dropdown.popupBox.children;
|
||||
is(options.length, 1, "Check dropdown has one remaining address");
|
||||
ok(options[0].textContent.includes("Mrs. Bar"), "Check remaining address");
|
||||
});
|
||||
|
||||
add_task(async function test_merchantError() {
|
||||
picker1.requestStore.setState({
|
||||
selectedShippingAddress: "68gjdh354j",
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
is(picker1.selectedStateKey, "selectedShippingAddress", "Check selectedStateKey");
|
||||
|
||||
let state = picker1.requestStore.getState();
|
||||
let {
|
||||
request,
|
||||
} = state;
|
||||
ok(!picker1.classList.contains("invalid-selected-option"), "No validation on a valid option");
|
||||
ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
|
||||
|
||||
let requestWithShippingAddressErrors = deepClone(request);
|
||||
Object.assign(requestWithShippingAddressErrors.paymentDetails, {
|
||||
shippingAddressErrors: {
|
||||
country: "Your country is not supported",
|
||||
},
|
||||
});
|
||||
picker1.requestStore.setState({
|
||||
request: requestWithShippingAddressErrors,
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(picker1.classList.contains("invalid-selected-option"), "The merchant error applies");
|
||||
ok(!isHidden(picker1.invalidLabel), "The merchant error should be visible");
|
||||
is(picker1.invalidLabel.innerText, "Your country is not supported", "Check displayed error text");
|
||||
|
||||
info("update the request to remove the errors");
|
||||
picker1.requestStore.setState({
|
||||
request,
|
||||
});
|
||||
await asyncElementRendered();
|
||||
ok(!picker1.classList.contains("invalid-selected-option"),
|
||||
"No errors visible when merchant errors cleared");
|
||||
ok(isHidden(picker1.invalidLabel), "The invalid label should be hidden");
|
||||
|
||||
info("Set billing address and payer errors which aren't relevant to this picker");
|
||||
let requestWithNonShippingAddressErrors = deepClone(request);
|
||||
Object.assign(requestWithNonShippingAddressErrors.paymentDetails, {
|
||||
payerErrors: {
|
||||
name: "Your name is too short",
|
||||
},
|
||||
paymentMethodErrors: {
|
||||
billingAddress: {
|
||||
country: "Your billing country is not supported",
|
||||
},
|
||||
},
|
||||
shippingAddressErrors: {},
|
||||
});
|
||||
picker1.requestStore.setState({
|
||||
request: requestWithNonShippingAddressErrors,
|
||||
});
|
||||
await asyncElementRendered();
|
||||
ok(!picker1.classList.contains("invalid-selected-option"), "No errors on a shipping picker");
|
||||
ok(isHidden(picker1.invalidLabel), "The invalid label should still be hidden");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,625 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the basic-card-form element
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the basic-card-form element</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="sinon-7.2.7.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
<script src="autofillEditForms.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/accepted-cards.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display" style="height: 100vh; margin: 0;">
|
||||
<iframe id="templateFrame" src="paymentRequest.xhtml" width="0" height="0"
|
||||
sandbox="allow-same-origin"
|
||||
style="float: left;"></iframe>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the basic-card-form element **/
|
||||
|
||||
/* global sinon */
|
||||
|
||||
import BasicCardForm from "../../res/containers/basic-card-form.js";
|
||||
|
||||
let display = document.getElementById("display");
|
||||
let supportedNetworks = ["discover", "amex"];
|
||||
let paymentMethods = [{
|
||||
supportedMethods: "basic-card",
|
||||
data: {
|
||||
supportedNetworks,
|
||||
},
|
||||
}];
|
||||
|
||||
function checkCCForm(customEl, expectedCard) {
|
||||
const CC_PROPERTY_NAMES = [
|
||||
"billingAddressGUID",
|
||||
"cc-number",
|
||||
"cc-name",
|
||||
"cc-exp-month",
|
||||
"cc-exp-year",
|
||||
"cc-type",
|
||||
];
|
||||
for (let propName of CC_PROPERTY_NAMES) {
|
||||
let expectedVal = expectedCard[propName] || "";
|
||||
is(document.getElementById(propName).value,
|
||||
expectedVal.toString(),
|
||||
`Check ${propName}`);
|
||||
}
|
||||
}
|
||||
|
||||
function createAddressRecord(source, props = {}) {
|
||||
let address = Object.assign({}, source, props);
|
||||
if (!address.name) {
|
||||
address.name = `${address["given-name"]} ${address["family-name"]}`;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
add_task(async function setup_once() {
|
||||
let templateFrame = document.getElementById("templateFrame");
|
||||
await SimpleTest.promiseFocus(templateFrame.contentWindow);
|
||||
let displayEl = document.getElementById("display");
|
||||
importDialogDependencies(templateFrame, displayEl);
|
||||
});
|
||||
|
||||
add_task(async function test_initialState() {
|
||||
let form = new BasicCardForm();
|
||||
|
||||
await form.requestStore.setState({
|
||||
savedAddresses: {
|
||||
"TimBLGUID": createAddressRecord(PTU.Addresses.TimBL),
|
||||
},
|
||||
});
|
||||
|
||||
let {page} = form.requestStore.getState();
|
||||
is(page.id, "payment-summary", "Check initial page");
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
is(page.id, "payment-summary", "Check initial page after appending");
|
||||
|
||||
// :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline.
|
||||
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
|
||||
for (let field of fieldsVisiblyInvalid) {
|
||||
info("invalid field: " + field.localName + "#" + field.id + "." + field.className);
|
||||
}
|
||||
is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_backButton() {
|
||||
let form = new BasicCardForm();
|
||||
form.dataset.backButtonLabel = "Back";
|
||||
form.dataset.addBasicCardTitle = "Sample page title 2";
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
});
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
let stateChangePromise = promiseStateChange(form.requestStore);
|
||||
is(form.pageTitleHeading.textContent, "Sample page title 2", "Check title");
|
||||
is(form.backButton.textContent, "Back", "Check label");
|
||||
form.backButton.scrollIntoView();
|
||||
synthesizeMouseAtCenter(form.backButton, {});
|
||||
|
||||
let {page} = await stateChangePromise;
|
||||
is(page.id, "payment-summary", "Check initial page after appending");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_saveButton() {
|
||||
let form = new BasicCardForm();
|
||||
form.dataset.nextButtonLabel = "Next";
|
||||
form.dataset.errorGenericSave = "Generic error";
|
||||
form.dataset.invalidAddressLabel = "Invalid";
|
||||
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
|
||||
let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
|
||||
let address2 = createAddressRecord(PTU.Addresses.TimBL2, {guid: "TimBL2GUID"});
|
||||
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentMethods,
|
||||
paymentDetails: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
[address2.guid]: deepClone(address2),
|
||||
},
|
||||
});
|
||||
|
||||
await asyncElementRendered();
|
||||
|
||||
// when merchant provides supportedNetworks, the accepted card list should be visible
|
||||
ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when adding a card");
|
||||
|
||||
ok(form.saveButton.disabled, "Save button should initially be disabled");
|
||||
fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111");
|
||||
form.form.querySelector("#cc-name").focus();
|
||||
// Check .disabled after .focus() so that it's after both "input" and "change" events.
|
||||
ok(form.saveButton.disabled, "Save button should still be disabled without a name");
|
||||
sendString("J. Smith");
|
||||
fillField(form.form.querySelector("#cc-exp-month"), "11");
|
||||
let year = (new Date()).getFullYear().toString();
|
||||
fillField(form.form.querySelector("#cc-exp-year"), year);
|
||||
fillField(form.form.querySelector("#cc-type"), "visa");
|
||||
fillField(form.form.querySelector("csc-input input"), "123");
|
||||
isnot(form.form.querySelector("#billingAddressGUID").value, address2.guid,
|
||||
"Check initial billing address");
|
||||
fillField(form.form.querySelector("#billingAddressGUID"), address2.guid);
|
||||
is(form.form.querySelector("#billingAddressGUID").value, address2.guid,
|
||||
"Check selected billing address");
|
||||
form.saveButton.focus();
|
||||
ok(!form.saveButton.disabled,
|
||||
"Save button should be enabled since the required fields are filled");
|
||||
|
||||
fillField(form.form.querySelector("#cc-exp-month"), "");
|
||||
fillField(form.form.querySelector("#cc-exp-year"), "");
|
||||
form.saveButton.focus();
|
||||
ok(form.saveButton.disabled,
|
||||
"Save button should be disabled since the required fields are empty");
|
||||
fillField(form.form.querySelector("#cc-exp-month"), "11");
|
||||
fillField(form.form.querySelector("#cc-exp-year"), year);
|
||||
form.saveButton.focus();
|
||||
ok(!form.saveButton.disabled,
|
||||
"Save button should be enabled since the required fields are filled again");
|
||||
|
||||
info("blanking the cc-number field");
|
||||
fillField(form.form.querySelector("#cc-number"), "");
|
||||
ok(form.saveButton.disabled, "Save button is disabled after blanking cc-number");
|
||||
form.form.querySelector("#cc-number").blur();
|
||||
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
|
||||
is(fieldsVisiblyInvalid.length, 1, "Check 1 field visibly invalid after blanking and blur");
|
||||
is(fieldsVisiblyInvalid[0].id, "cc-number", "Check #cc-number is visibly invalid");
|
||||
|
||||
fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111");
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0, "Check no fields visibly invalid");
|
||||
ok(!form.saveButton.disabled, "Save button is enabled after re-filling cc-number");
|
||||
|
||||
let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
|
||||
is(form.saveButton.textContent, "Next", "Check label");
|
||||
form.saveButton.scrollIntoView();
|
||||
synthesizeMouseAtCenter(form.saveButton, {});
|
||||
|
||||
let details = await messagePromise;
|
||||
ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
|
||||
delete details.messageID;
|
||||
is(details.collectionName, "creditCards", "Check collectionName");
|
||||
isDeeply(details, {
|
||||
collectionName: "creditCards",
|
||||
guid: undefined,
|
||||
messageType: "updateAutofillRecord",
|
||||
record: {
|
||||
"cc-exp-month": "11",
|
||||
"cc-exp-year": year,
|
||||
"cc-name": "J. Smith",
|
||||
"cc-number": "4111 1111-1111 1111",
|
||||
"cc-type": "visa",
|
||||
"billingAddressGUID": address2.guid,
|
||||
"isTemporary": true,
|
||||
},
|
||||
}, "Check event details for the message to chrome");
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_requiredAttributePropagated() {
|
||||
let form = new BasicCardForm();
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
|
||||
is(requiredElements.length, 7, "Number of required elements");
|
||||
for (let element of requiredElements) {
|
||||
if (element.id == "billingAddressGUID") {
|
||||
// The billing address has a different layout.
|
||||
continue;
|
||||
}
|
||||
let container = element.closest("label") || element.closest("div");
|
||||
ok(container.hasAttribute("required"),
|
||||
`Container ${container.id} should also be marked as required`);
|
||||
}
|
||||
// Now test that toggling the `required` attribute will affect the container.
|
||||
let sampleRequiredElement = requiredElements[0];
|
||||
let sampleRequiredContainer = sampleRequiredElement.closest("label") ||
|
||||
sampleRequiredElement.closest("div");
|
||||
sampleRequiredElement.removeAttribute("required");
|
||||
await form.requestStore.setState({});
|
||||
await asyncElementRendered();
|
||||
ok(!sampleRequiredElement.hasAttribute("required"),
|
||||
`"required" attribute should still be removed from element (${sampleRequiredElement.id})`);
|
||||
ok(!sampleRequiredContainer.hasAttribute("required"),
|
||||
`"required" attribute should be removed from container`);
|
||||
sampleRequiredElement.setAttribute("required", "true");
|
||||
await form.requestStore.setState({});
|
||||
await asyncElementRendered();
|
||||
ok(sampleRequiredContainer.hasAttribute("required"),
|
||||
"`required` attribute is re-added to container");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_genericError() {
|
||||
let form = new BasicCardForm();
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "test-page",
|
||||
error: "Generic Error",
|
||||
},
|
||||
});
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
ok(!isHidden(form.genericErrorText), "Error message should be visible");
|
||||
is(form.genericErrorText.textContent, "Generic Error", "Check error message");
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_add_selectedShippingAddress() {
|
||||
let form = new BasicCardForm();
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
info("have an existing card in storage");
|
||||
let card1 = deepClone(PTU.BasicCards.JohnDoe);
|
||||
card1.guid = "9864798564";
|
||||
card1["cc-exp-year"] = 2011;
|
||||
|
||||
let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
|
||||
let address2 = createAddressRecord(PTU.Addresses.TimBL2, { guid: "TimBL2GUID" });
|
||||
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
[address2.guid]: deepClone(address2),
|
||||
},
|
||||
savedBasicCards: {
|
||||
[card1.guid]: deepClone(card1),
|
||||
},
|
||||
selectedShippingAddress: address2.guid,
|
||||
});
|
||||
await asyncElementRendered();
|
||||
checkCCForm(form, {
|
||||
billingAddressGUID: address2.guid,
|
||||
});
|
||||
|
||||
form.remove();
|
||||
await form.requestStore.reset();
|
||||
});
|
||||
|
||||
add_task(async function test_add_noSelectedShippingAddress() {
|
||||
let form = new BasicCardForm();
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
info("have an existing card in storage but unused");
|
||||
let card1 = deepClone(PTU.BasicCards.JohnDoe);
|
||||
card1.guid = "9864798564";
|
||||
card1["cc-exp-year"] = 2011;
|
||||
|
||||
let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
|
||||
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
savedBasicCards: {
|
||||
[card1.guid]: deepClone(card1),
|
||||
},
|
||||
selectedShippingAddress: null,
|
||||
});
|
||||
await asyncElementRendered();
|
||||
checkCCForm(form, {
|
||||
billingAddressGUID: address1.guid,
|
||||
});
|
||||
|
||||
info("now test with a missing selectedShippingAddress");
|
||||
await form.requestStore.setState({
|
||||
selectedShippingAddress: "some-missing-guid",
|
||||
});
|
||||
await asyncElementRendered();
|
||||
checkCCForm(form, {
|
||||
billingAddressGUID: address1.guid,
|
||||
});
|
||||
|
||||
form.remove();
|
||||
await form.requestStore.reset();
|
||||
});
|
||||
|
||||
add_task(async function test_edit() {
|
||||
let form = new BasicCardForm();
|
||||
form.dataset.updateButtonLabel = "Update";
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
let address1 = createAddressRecord(PTU.Addresses.TimBL, { guid: "TimBLGUID" });
|
||||
|
||||
info("test year before current");
|
||||
let card1 = deepClone(PTU.BasicCards.JohnDoe);
|
||||
card1.guid = "9864798564";
|
||||
card1["cc-exp-year"] = 2011;
|
||||
card1.billingAddressGUID = address1.guid;
|
||||
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentMethods,
|
||||
paymentDetails: {},
|
||||
},
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
guid: card1.guid,
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
savedBasicCards: {
|
||||
[card1.guid]: deepClone(card1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
is(form.saveButton.textContent, "Update", "Check label");
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check no fields are visibly invalid on an 'edit' form with a complete card");
|
||||
|
||||
checkCCForm(form, card1);
|
||||
ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid card");
|
||||
ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when editing a card");
|
||||
|
||||
let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
|
||||
ok(requiredElements.length, "There should be at least one required element");
|
||||
is(requiredElements.length, 5, "Number of required elements");
|
||||
for (let element of requiredElements) {
|
||||
if (element.id == "billingAddressGUID") {
|
||||
// The billing address has a different layout.
|
||||
continue;
|
||||
}
|
||||
|
||||
let container = element.closest("label") || element.closest("div");
|
||||
ok(element.hasAttribute("required"), "Element should be marked as required");
|
||||
ok(container.hasAttribute("required"), "Container should also be marked as required");
|
||||
}
|
||||
|
||||
info("test future year");
|
||||
card1["cc-exp-year"] = 2100;
|
||||
|
||||
await form.requestStore.setState({
|
||||
savedBasicCards: {
|
||||
[card1.guid]: deepClone(card1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
checkCCForm(form, card1);
|
||||
|
||||
info("test change to minimal record");
|
||||
let minimalCard = {
|
||||
// no expiration date or name
|
||||
"cc-number": "1234567690123",
|
||||
guid: "9gnjdhen46",
|
||||
};
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
guid: minimalCard.guid,
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
savedBasicCards: {
|
||||
[minimalCard.guid]: deepClone(minimalCard),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
ok(form.querySelectorAll(":-moz-ui-invalid").length > 0,
|
||||
"Check fields are visibly invalid on an 'edit' form with missing fields");
|
||||
checkCCForm(form, minimalCard);
|
||||
|
||||
info("change to no selected card");
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
guid: null,
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
|
||||
"Check no fields are visibly invalid after converting to an 'add' form");
|
||||
checkCCForm(form, {
|
||||
billingAddressGUID: address1.guid, // Default selected
|
||||
});
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_field_validity_updates() {
|
||||
let form = new BasicCardForm();
|
||||
form.dataset.updateButtonLabel = "Update";
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
|
||||
let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentMethods,
|
||||
paymentDetails: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
let ccNumber = form.form.querySelector("#cc-number");
|
||||
let nameInput = form.form.querySelector("#cc-name");
|
||||
let typeInput = form.form.querySelector("#cc-type");
|
||||
let cscInput = form.form.querySelector("csc-input input");
|
||||
let monthInput = form.form.querySelector("#cc-exp-month");
|
||||
let yearInput = form.form.querySelector("#cc-exp-year");
|
||||
let addressPicker = form.querySelector("#billingAddressGUID");
|
||||
|
||||
info("test with valid cc-number but missing cc-name");
|
||||
fillField(ccNumber, "4111111111111111");
|
||||
ok(ccNumber.checkValidity(), "cc-number field is valid with good input");
|
||||
ok(!nameInput.checkValidity(), "cc-name field is invalid when empty");
|
||||
ok(form.saveButton.disabled, "Save button should be disabled with incomplete input");
|
||||
|
||||
info("correct by adding cc-name and expiration values");
|
||||
fillField(nameInput, "First");
|
||||
fillField(monthInput, "11");
|
||||
let year = (new Date()).getFullYear().toString();
|
||||
fillField(yearInput, year);
|
||||
fillField(typeInput, "visa");
|
||||
fillField(cscInput, "456");
|
||||
ok(ccNumber.checkValidity(), "cc-number field is valid with good input");
|
||||
ok(nameInput.checkValidity(), "cc-name field is valid with a value");
|
||||
ok(monthInput.checkValidity(), "cc-exp-month field is valid with a value");
|
||||
ok(yearInput.checkValidity(), "cc-exp-year field is valid with a value");
|
||||
ok(typeInput.checkValidity(), "cc-type field is valid with a value");
|
||||
|
||||
// should auto-select the first billing address
|
||||
ok(addressPicker.value, "An address is selected: " + addressPicker.value);
|
||||
|
||||
let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
|
||||
for (let field of fieldsVisiblyInvalid) {
|
||||
info("invalid field: " + field.localName + "#" + field.id + "." + field.className);
|
||||
}
|
||||
is(fieldsVisiblyInvalid.length, 0, "No fields are visibly invalid");
|
||||
|
||||
ok(!form.saveButton.disabled, "Save button should not be disabled with good input");
|
||||
|
||||
info("edit to make the cc-number invalid");
|
||||
ccNumber.focus();
|
||||
sendString("aa");
|
||||
nameInput.focus();
|
||||
sendString("Surname");
|
||||
|
||||
ok(!ccNumber.checkValidity(), "cc-number field becomes invalid with bad input");
|
||||
ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid");
|
||||
ok(nameInput.checkValidity(), "cc-name field is valid with a value");
|
||||
ok(form.saveButton.disabled, "Save button becomes disabled with bad input");
|
||||
|
||||
info("fix the cc-number to make it all valid again");
|
||||
ccNumber.focus();
|
||||
sendKey("BACK_SPACE");
|
||||
sendKey("BACK_SPACE");
|
||||
info("after backspaces, ccNumber.value: " + ccNumber.value);
|
||||
|
||||
ok(ccNumber.checkValidity(), "cc-number field becomes valid with corrected input");
|
||||
ok(nameInput.checkValidity(), "cc-name field is valid with a value");
|
||||
ok(!form.saveButton.disabled, "Save button is no longer disabled with corrected input");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_numberCustomValidityReset() {
|
||||
let form = new BasicCardForm();
|
||||
form.dataset.updateButtonLabel = "Update";
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
|
||||
let address1 = createAddressRecord(PTU.Addresses.TimBL, {guid: "TimBLGUID"});
|
||||
await form.requestStore.setState({
|
||||
request: {
|
||||
paymentMethods,
|
||||
paymentDetails: {},
|
||||
},
|
||||
savedAddresses: {
|
||||
[address1.guid]: deepClone(address1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
fillField(form.querySelector("#cc-number"), "junk");
|
||||
sendKey("TAB");
|
||||
ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid");
|
||||
|
||||
info("simulate triggering an add again to reset the form");
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
});
|
||||
|
||||
ok(!form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is not visibly invalid");
|
||||
|
||||
form.remove();
|
||||
});
|
||||
|
||||
add_task(async function test_noCardNetworkSelected() {
|
||||
let form = new BasicCardForm();
|
||||
await form.promiseReady;
|
||||
display.appendChild(form);
|
||||
await asyncElementRendered();
|
||||
|
||||
info("have an existing card in storage, with no network id");
|
||||
let card1 = deepClone(PTU.BasicCards.JohnDoe);
|
||||
card1.guid = "9864798564";
|
||||
delete card1["cc-type"];
|
||||
|
||||
await form.requestStore.setState({
|
||||
page: {
|
||||
id: "basic-card-page",
|
||||
},
|
||||
"basic-card-page": {
|
||||
guid: card1.guid,
|
||||
selectedStateKey: "selectedPaymentCard",
|
||||
},
|
||||
savedBasicCards: {
|
||||
[card1.guid]: deepClone(card1),
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
checkCCForm(form, card1);
|
||||
is(document.getElementById("cc-type").selectedIndex, 0, "Initial empty option is selected");
|
||||
|
||||
form.remove();
|
||||
await form.requestStore.reset();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,96 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the basic-card-option component
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the basic-card-option component</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/basic-card-option.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<option id="option1"
|
||||
value="option1"
|
||||
cc-exp="2024-06"
|
||||
cc-name="John Smith"
|
||||
cc-number="************5461"
|
||||
cc-type="visa"
|
||||
guid="option1"></option>
|
||||
<option id="option2"
|
||||
value="option2"
|
||||
cc-number="************1111"
|
||||
guid="option2"></option>
|
||||
|
||||
<rich-select id="richSelect1"
|
||||
option-type="basic-card-option"></rich-select>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the basic-card-option component **/
|
||||
|
||||
import "../../res/components/basic-card-option.js";
|
||||
import "../../res/components/rich-select.js";
|
||||
|
||||
let option1 = document.getElementById("option1");
|
||||
let option2 = document.getElementById("option2");
|
||||
let richSelect1 = document.getElementById("richSelect1");
|
||||
|
||||
add_task(async function test_populated_option_rendering() {
|
||||
richSelect1.popupBox.appendChild(option1);
|
||||
richSelect1.value = option1.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
is(richOption.ccExp, "2024-06", "Check ccExp getter");
|
||||
is(richOption.ccName, "John Smith", "Check ccName getter");
|
||||
is(richOption.ccNumber, "************5461", "Check ccNumber getter");
|
||||
is(richOption.ccType, "visa", "Check ccType getter");
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
// Note that innerText takes visibility into account so that's why it's used over textContent here
|
||||
is(richOption["_cc-exp"].innerText, "Exp. 2024-06", "cc-exp text");
|
||||
is(richOption["_cc-name"].innerText, "John Smith", "cc-name text");
|
||||
is(richOption["_cc-number"].innerText, "****5461", "cc-number text");
|
||||
is(richOption["_cc-type"].localName, "img", "cc-type localName");
|
||||
is(richOption["_cc-type"].alt, "visa", "cc-type img alt");
|
||||
});
|
||||
|
||||
add_task(async function test_minimal_option_rendering() {
|
||||
richSelect1.popupBox.appendChild(option2);
|
||||
richSelect1.value = option2.value;
|
||||
await asyncElementRendered();
|
||||
|
||||
let richOption = richSelect1.selectedRichOption;
|
||||
is(richOption.ccExp, null, "Check ccExp getter");
|
||||
is(richOption.ccName, null, "Check ccName getter");
|
||||
is(richOption.ccNumber, "************1111", "Check ccNumber getter");
|
||||
is(richOption.ccType, null, "Check ccType getter");
|
||||
|
||||
ok(!richOption.innerText.includes("undefined"), "Check for presence of 'undefined'");
|
||||
ok(!richOption.innerText.includes("null"), "Check for presence of 'null'");
|
||||
|
||||
is(richOption["_cc-exp"].innerText, "", "cc-exp text");
|
||||
is(richOption["_cc-name"].innerText, "", "cc-name text");
|
||||
is(richOption["_cc-number"].innerText, "****1111", "cc-number text");
|
||||
is(richOption["_cc-type"].localName, "img", "cc-type localName");
|
||||
is(richOption["_cc-type"].alt, "", "cc-type img alt");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,132 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the address-picker component
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the billing-address-picker component</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/EventUtils.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
<script src="autofillEditForms.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="../../res/containers/rich-picker.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/rich-select.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="../../res/components/address-option.css"/>
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<billing-address-picker id="picker1"
|
||||
data-field-separator=", "
|
||||
data-invalid-label="Picker1: Missing or Invalid"
|
||||
selected-state-key="basic-card-page|billingAddressGUID"></billing-address-picker>
|
||||
<select id="theOptions">
|
||||
<option></option>
|
||||
<option value="48bnds6854t">48bnds6854t</option>
|
||||
<option value="68gjdh354j" selected="">68gjdh354j</option>
|
||||
</select>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the billing-address-picker component **/
|
||||
|
||||
import BillingAddressPicker from "../../res/containers/billing-address-picker.js";
|
||||
|
||||
let picker1 = document.getElementById("picker1");
|
||||
let addresses = {
|
||||
"48bnds6854t": {
|
||||
"address-level1": "MI",
|
||||
"address-level2": "Some City",
|
||||
"country": "US",
|
||||
"guid": "48bnds6854t",
|
||||
"name": "Mr. Foo",
|
||||
"postal-code": "90210",
|
||||
"street-address": "123 Sesame Street,\nApt 40",
|
||||
"tel": "+1 519 555-5555",
|
||||
timeLastUsed: 200,
|
||||
},
|
||||
"68gjdh354j": {
|
||||
"address-level1": "CA",
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "68gjdh354j",
|
||||
"name": "Mrs. Bar",
|
||||
"postal-code": "94041",
|
||||
"street-address": "P.O. Box 123",
|
||||
"tel": "+1 650 555-5555",
|
||||
timeLastUsed: 300,
|
||||
},
|
||||
"abcde12345": {
|
||||
"address-level2": "Mountain View",
|
||||
"country": "US",
|
||||
"guid": "abcde12345",
|
||||
"name": "Mrs. Fields",
|
||||
timeLastUsed: 100,
|
||||
},
|
||||
};
|
||||
|
||||
add_task(async function test_empty() {
|
||||
ok(picker1, "Check picker1 exists");
|
||||
let {savedAddresses} = picker1.requestStore.getState();
|
||||
is(Object.keys(savedAddresses).length, 0, "Check empty initial state");
|
||||
is(picker1.editLink.hidden, true, "Check that picker edit link is hidden");
|
||||
is(picker1.options.length, 1, "Check only the empty option is present");
|
||||
ok(picker1.dropdown.selectedOption, "Has a selectedOption");
|
||||
is(picker1.dropdown.value, "", "Has empty value");
|
||||
|
||||
// update state to trigger render without changing available addresses
|
||||
picker1.requestStore.setState({
|
||||
"basic-card-page": {
|
||||
"someKey": "someValue",
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
is(picker1.dropdown.popupBox.children.length, 1, "Check only the empty option is present");
|
||||
ok(picker1.dropdown.selectedOption, "Has a selectedOption");
|
||||
is(picker1.dropdown.value, "", "Has empty value");
|
||||
});
|
||||
|
||||
add_task(async function test_getCurrentValue() {
|
||||
picker1.requestStore.setState({
|
||||
"basic-card-page": {
|
||||
"billingAddressGUID": "68gjdh354j",
|
||||
},
|
||||
savedAddresses: addresses,
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
picker1.dropdown.popupBox.value = "abcde12345";
|
||||
|
||||
is(picker1.options.length, 4, "Check we have options for each address + empty one");
|
||||
is(picker1.getCurrentValue(picker1.requestStore.getState()), "abcde12345",
|
||||
"Initial/current value reflects the <select>.value, " +
|
||||
"not whatever is in the state at the selectedStateKey");
|
||||
});
|
||||
|
||||
add_task(async function test_wrapPopupBox() {
|
||||
let picker = new BillingAddressPicker();
|
||||
picker.dropdown.popupBox = document.querySelector("#theOptions");
|
||||
picker.dataset.invalidLabel = "Invalid";
|
||||
picker.setAttribute("label", "The label");
|
||||
picker.setAttribute("selected-state-key", "basic-card-page|billingAddressGUID");
|
||||
|
||||
document.querySelector("#display").appendChild(picker);
|
||||
|
||||
is(picker.labelElement.getAttribute("for"), "theOptions",
|
||||
"The label points at the right element");
|
||||
is(picker.invalidLabel.getAttribute("for"), "theOptions",
|
||||
"The invalidLabel points at the right element");
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
@ -1,88 +0,0 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Test the completion-error-page component
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Test the completion-error-page component</title>
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="payments_common.js"></script>
|
||||
<script src="../../res/unprivileged-fallbacks.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
|
||||
</head>
|
||||
<body>
|
||||
<p id="display">
|
||||
<completion-error-page id="completion-timeout-error" class="illustrated"
|
||||
data-page-title="Sample Title"
|
||||
data-suggestion-heading="Sample suggestion heading"
|
||||
data-suggestion-1="Sample suggestion"
|
||||
data-suggestion-2="Sample suggestion"
|
||||
data-suggestion-3="Sample suggestion"
|
||||
data-branding-label="Sample Brand"
|
||||
data-done-button-label="OK"></completion-error-page>
|
||||
</p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
</pre>
|
||||
<script type="module">
|
||||
/** Test the completion-error-page component **/
|
||||
|
||||
import "../../res/containers/completion-error-page.js";
|
||||
|
||||
let page = document.getElementById("completion-timeout-error");
|
||||
|
||||
add_task(async function test_no_values() {
|
||||
ok(page, "page exists");
|
||||
is(page.dataset.pageTitle, "Sample Title", "Title set on page");
|
||||
is(page.dataset.suggestionHeading, "Sample suggestion heading",
|
||||
"Suggestion heading set on page");
|
||||
is(page.dataset["suggestion-1"], "Sample suggestion",
|
||||
"Suggestion 1 set on page");
|
||||
is(page.dataset["suggestion-2"], "Sample suggestion",
|
||||
"Suggestion 2 set on page");
|
||||
is(page.dataset["suggestion-3"], "Sample suggestion",
|
||||
"Suggestion 3 set on page");
|
||||
is(page.dataset.brandingLabel, "Sample Brand", "Branding string set");
|
||||
|
||||
page.dataset.pageTitle = "Oh noes! **host-name** is having an issue";
|
||||
page.dataset["suggestion-2"] = "You should probably blame **host-name**, not us";
|
||||
const displayHost = "allizom.com";
|
||||
let request = { topLevelPrincipal: { URI: { displayHost } } };
|
||||
await page.requestStore.setState({
|
||||
changesPrevented: false,
|
||||
request: Object.assign({}, request, {completeStatus: ""}),
|
||||
orderDetailsShowing: false,
|
||||
page: {
|
||||
id: "completion-timeout-error",
|
||||
},
|
||||
});
|
||||
await asyncElementRendered();
|
||||
|
||||
is(page.requestStore.getState().request.topLevelPrincipal.URI.displayHost, displayHost,
|
||||
"State should have the displayHost set properly");
|
||||
is(page.querySelector("h2").textContent,
|
||||
`Oh noes! ${displayHost} is having an issue`,
|
||||
"Title includes host-name");
|
||||
is(page.querySelector("p").textContent,
|
||||
"Sample suggestion heading",
|
||||
"Suggestion heading set on page");
|
||||
is(page.querySelector("li:nth-child(1)").textContent, "Sample suggestion",
|
||||
"Suggestion 1 set on page");
|
||||
is(page.querySelector("li:nth-child(2)").textContent,
|
||||
`You should probably blame ${displayHost}, not us`,
|
||||
"Suggestion 2 includes host-name");
|
||||
is(page.querySelector(".branding").textContent,
|
||||
"Sample Brand",
|
||||
"Branding set on page");
|
||||
is(page.querySelector(".primary").textContent,
|
||||
"OK",
|
||||
"Primary button label set correctly");
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user