Bug 1434508 - better serialization of paymentrequest data. r?MattN
* Add a serializeRequest method to paymentDialogWrapper to correctly serialize the nsIArray values
* Test results of serializing a request with multiple displayItems, shippingOptions, paymentMethods and modifiers
MozReview-Commit-ID: DTqzTAjvdxq
--- a/toolkit/components/payments/content/paymentDialogWrapper.js
+++ b/toolkit/components/payments/content/paymentDialogWrapper.js
@@ -197,26 +197,95 @@ var paymentDialogWrapper = {
messageType: "updateState",
data: {
savedAddresses: this.fetchSavedAddresses(),
savedBasicCards: this.fetchSavedPaymentCards(),
},
});
},
- initializeFrame() {
- let requestSerialized = JSON.parse(JSON.stringify(this.request));
+ /**
+ * 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;
+ },
- // Manually serialize the nsIPrincipal.
- let displayHost = this.request.topLevelPrincipal.URI.displayHost;
- requestSerialized.topLevelPrincipal = {
- URI: {
- displayHost,
- },
- };
+ initializeFrame() {
+ let requestSerialized = this._serializeRequest(this.request);
this.mm.sendAsyncMessage("paymentChromeToContent", {
messageType: "showPaymentRequest",
data: {
request: requestSerialized,
savedAddresses: this.fetchSavedAddresses(),
savedBasicCards: this.fetchSavedPaymentCards(),
},
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm
+++ b/toolkit/components/payments/test/PaymentTestUtils.jsm
@@ -74,22 +74,103 @@ this.PaymentTestUtils = {
/**
* Common PaymentMethodData for testing
*/
MethodData: {
basicCard: {
supportedMethods: "basic-card",
},
+ bobPay: {
+ supportedMethods: "https://www.example.com/bobpay",
+ },
},
/**
* Common PaymentDetailsInit for testing
*/
Details: {
total60USD: {
total: {
label: "Total due",
amount: { currency: "USD", value: "60.00" },
},
},
+ twoDisplayItems: {
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "32.00" },
+ },
+ displayItems: [
+ {
+ label: "First",
+ amount: { currency: "USD", value: "1" },
+ },
+ {
+ label: "Second",
+ amount: { currency: "USD", value: "2" },
+ },
+ ],
+ },
+ twoShippingOptions: {
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "2.00" },
+ },
+ 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,
+ },
+ ],
+ },
+ bobPayPaymentModifier: {
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "2.00" },
+ },
+ displayItems: [
+ {
+ label: "First",
+ amount: { currency: "USD", value: "1" },
+ },
+ ],
+ 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: {
+ supportedTypes: "credit",
+ },
+ },
+ {
+ 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" },
+ },
+ },
+ ],
+ },
},
};
--- a/toolkit/components/payments/test/browser/browser.ini
+++ b/toolkit/components/payments/test/browser/browser.ini
@@ -3,12 +3,13 @@ head = head.js
prefs =
dom.payments.request.enabled=true
skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s
support-files =
blank_page.html
[browser_host_name.js]
[browser_profile_storage.js]
+[browser_request_serialization.js]
[browser_request_summary.js]
[browser_show_dialog.js]
skip-if = os == 'win' && debug # bug 1418385
[browser_total.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/components/payments/test/browser/browser_request_serialization.js
@@ -0,0 +1,121 @@
+"use strict";
+
+// Disable CPOW checks because they have false-positives from use of ContentTask in a helper.
+/* eslint-disable mozilla/no-cpows-in-tests */
+
+add_task(async function test_serializeRequest_displayItems() {
+ const testTask = ({methodData, details}) => {
+ let contentWin = Components.utils.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: PTU.Details.twoDisplayItems,
+ };
+ await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_serializeRequest_shippingOptions() {
+ const testTask = ({methodData, details}) => {
+ let contentWin = Components.utils.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.shippingOptions) {
+ 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];
+ is(item.label, expectedItem.label, "shippingOption label matches");
+ is(item.amount.value, expectedItem.amount.value, "shippingOption label matches");
+ is(item.amount.currency, expectedItem.amount.currency, "shippingOption label matches");
+ }
+ } else {
+ is(actual.shippingOptions, null, "falsey input shippingOptions is serialized to null");
+ }
+ };
+
+ const args = {
+ methodData: [PTU.MethodData.basicCard],
+ details: PTU.Details.twoShippingOptions,
+ };
+ await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_serializeRequest_paymentMethods() {
+ const testTask = ({methodData, details}) => {
+ let contentWin = Components.utils.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");
+ };
+ const args = {
+ methodData: [PTU.MethodData.basicCard, PTU.MethodData.bobPay],
+ details: PTU.Details.total60USD,
+ };
+ await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});
+
+add_task(async function test_serializeRequest_modifiers() {
+ const testTask = ({methodData, details}) => {
+ let contentWin = Components.utils.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.bobPay],
+ details: PTU.Details.bobPayPaymentModifier,
+ };
+ await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, testTask, args);
+});