Remove payments

This commit is contained in:
Fedor 2023-10-30 22:47:59 +02:00
parent 1b44b5877f
commit 1d3013a6f8
428 changed files with 2 additions and 52953 deletions

View File

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

View File

@ -269,7 +269,6 @@ module.exports = {
"dom/messagechannel/**",
"dom/midi/**",
"dom/network/**",
"dom/payments/**",
"dom/performance/**",
"dom/permission/**",
"dom/quota/**",

View File

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

View File

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

View File

@ -56,7 +56,6 @@ DIRS += ['build']
if CONFIG['NIGHTLY_BUILD']:
DIRS += [
'aboutconfig',
'payments',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':

View File

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

View File

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

View File

@ -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',
},
]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
// (&nbsp; 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);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +0,0 @@
payment-details-item {
margin: 1px 0;
min-height: 2em;
}
payment-details-item > currency-amount {
text-align: end;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 "Cant ship to this address. Select a different address.">
<!ENTITY deliveryGenericError "Cant deliver to this address. Select a different address.">
<!ENTITY pickupGenericError "Cant 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 dont 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 dont have to type it every time.">
<!ENTITY failErrorPage.title "We couldnt 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 youre using hasnt 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>

View File

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

View File

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

View File

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

View File

@ -1,10 +0,0 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Blank page</title>
</head>
<body>
BLANK PAGE
</body>
</html>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,&#xA;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>

View File

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

View File

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

View File

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

View File

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

View File

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