| <!DOCTYPE html> |
| <!-- Copyright © 2017 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> |
| <meta charset="utf-8"> |
| <title>Test for PaymentRequest Constructor</title> |
| <link rel="help" href="https://w3c.github.io/browser-payment-api/#constructor"> |
| <script src="/resources/testharness.js"></script> |
| <script src="/resources/testharnessreport.js"></script> |
| <script> |
| "use strict"; |
| const testMethod = Object.freeze({ |
| supportedMethods: "https://wpt.fyi/payment-request", |
| }); |
| const defaultMethods = Object.freeze([testMethod]); |
| const defaultAmount = Object.freeze({ |
| currency: "USD", |
| value: "1.0", |
| }); |
| const defaultNumberAmount = Object.freeze({ |
| currency: "USD", |
| value: 1.0, |
| }); |
| const defaultTotal = Object.freeze({ |
| label: "Default Total", |
| amount: defaultAmount, |
| }); |
| const defaultNumberTotal = Object.freeze({ |
| label: "Default Number Total", |
| amount: defaultNumberAmount, |
| }); |
| const defaultDetails = Object.freeze({ |
| total: defaultTotal, |
| displayItems: [ |
| { |
| label: "Default Display Item", |
| amount: defaultAmount, |
| }, |
| ], |
| }); |
| const defaultNumberDetails = Object.freeze({ |
| total: defaultNumberTotal, |
| displayItems: [ |
| { |
| label: "Default Display Item", |
| amount: defaultNumberAmount, |
| }, |
| ], |
| }); |
| |
| // Avoid false positives, this should always pass |
| function smokeTest() { |
| new PaymentRequest(defaultMethods, defaultDetails); |
| new PaymentRequest(defaultMethods, defaultNumberDetails); |
| } |
| test(() => { |
| smokeTest(); |
| const request = new PaymentRequest(defaultMethods, defaultDetails); |
| assert_true(Boolean(request.id), "must be some truthy value"); |
| }, "If details.id is missing, assign an identifier"); |
| |
| test(() => { |
| smokeTest(); |
| const request1 = new PaymentRequest(defaultMethods, defaultDetails); |
| const request2 = new PaymentRequest(defaultMethods, defaultDetails); |
| assert_not_equals(request1.id, request2.id, "UA generated ID must be unique"); |
| const seen = new Set(); |
| // Let's try creating lots of requests, and make sure they are all unique |
| for (let i = 0; i < 1024; i++) { |
| const request = new PaymentRequest(defaultMethods, defaultDetails); |
| assert_false( |
| seen.has(request.id), |
| `UA generated ID must be unique, but got duplicate! (${request.id})` |
| ); |
| seen.add(request.id); |
| } |
| }, "If details.id is missing, assign a unique identifier"); |
| |
| test(() => { |
| smokeTest(); |
| const newDetails = Object.assign({}, defaultDetails, { id: "test123" }); |
| const request1 = new PaymentRequest(defaultMethods, newDetails); |
| const request2 = new PaymentRequest(defaultMethods, newDetails); |
| assert_equals(request1.id, newDetails.id, `id must be ${newDetails.id}`); |
| assert_equals(request2.id, newDetails.id, `id must be ${newDetails.id}`); |
| assert_equals(request1.id, request2.id, "ids need to be the same"); |
| }, "If the same id is provided, then use it"); |
| |
| test(() => { |
| smokeTest(); |
| const newDetails = Object.assign({}, defaultDetails, { |
| id: "".padStart(1024, "a"), |
| }); |
| const request = new PaymentRequest(defaultMethods, newDetails); |
| assert_equals( |
| request.id, |
| newDetails.id, |
| `id must be provided value, even if very long and contain spaces` |
| ); |
| }, "Use ids even if they are strange"); |
| |
| test(() => { |
| smokeTest(); |
| const request = new PaymentRequest( |
| defaultMethods, |
| Object.assign({}, defaultDetails, { id: "foo" }) |
| ); |
| assert_equals(request.id, "foo"); |
| }, "Use provided request ID"); |
| |
| test(() => { |
| smokeTest(); |
| assert_throws(new TypeError(), () => new PaymentRequest([], defaultDetails)); |
| }, "If the length of the methodData sequence is zero, then throw a TypeError"); |
| |
| test(() => { |
| smokeTest(); |
| const JSONSerializables = [[], { object: {} }]; |
| for (const data of JSONSerializables) { |
| try { |
| const methods = [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data, |
| }, |
| ]; |
| new PaymentRequest(methods, defaultDetails); |
| } catch (err) { |
| assert_unreached( |
| `Unexpected error parsing stringifiable JSON: ${JSON.stringify( |
| data |
| )}: ${err.message}` |
| ); |
| } |
| } |
| }, "Modifier method data must be JSON-serializable object"); |
| |
| test(() => { |
| smokeTest(); |
| const recursiveDictionary = {}; |
| recursiveDictionary.foo = recursiveDictionary; |
| assert_throws(new TypeError(), () => { |
| const methods = [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data: recursiveDictionary, |
| }, |
| ]; |
| new PaymentRequest(methods, defaultDetails); |
| }); |
| assert_throws(new TypeError(), () => { |
| const methods = [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data: "a string", |
| }, |
| ]; |
| new PaymentRequest(methods, defaultDetails); |
| }); |
| assert_throws( |
| new TypeError(), |
| () => { |
| const methods = [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data: null, |
| }, |
| ]; |
| new PaymentRequest(methods, defaultDetails); |
| }, |
| "Even though null is JSON-serializable, it's not type 'Object' per ES spec" |
| ); |
| }, "Rethrow any exceptions of JSON-serializing paymentMethod.data into a string"); |
| |
| // process total |
| const invalidAmounts = [ |
| "-", |
| "notdigits", |
| "ALSONOTDIGITS", |
| "10.", |
| ".99", |
| "-10.", |
| "-.99", |
| "10-", |
| "1-0", |
| "1.0.0", |
| "1/3", |
| "", |
| null, |
| " 1.0 ", |
| " 1.0 ", |
| "1.0 ", |
| "USD$1.0", |
| "$1.0", |
| { |
| toString() { |
| return " 1.0"; |
| }, |
| }, |
| ]; |
| const invalidTotalAmounts = invalidAmounts.concat([ |
| "-1", |
| "-1.0", |
| "-1.00", |
| "-1000.000", |
| -10, |
| ]); |
| test(() => { |
| smokeTest(); |
| for (const invalidAmount of invalidTotalAmounts) { |
| const invalidDetails = { |
| total: { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: invalidAmount, |
| }, |
| }, |
| }; |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, invalidDetails); |
| }, |
| `Expect TypeError when details.total.amount.value is ${invalidAmount}` |
| ); |
| } |
| }, `If details.total.amount.value is not a valid decimal monetary value, then throw a TypeError`); |
| |
| test(() => { |
| smokeTest(); |
| for (const prop in ["displayItems", "shippingOptions", "modifiers"]) { |
| try { |
| const details = Object.assign({}, defaultDetails, { [prop]: [] }); |
| new PaymentRequest(defaultMethods, details); |
| assert_unreached(`PaymentDetailsBase.${prop} can be zero length`); |
| } catch (err) {} |
| } |
| }, `PaymentDetailsBase members can be 0 length`); |
| |
| test(() => { |
| smokeTest(); |
| assert_throws(new TypeError(), () => { |
| new PaymentRequest(defaultMethods, { |
| total: { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: "-1.00", |
| }, |
| }, |
| }); |
| }); |
| }, "If the first character of details.total.amount.value is U+002D HYPHEN-MINUS, then throw a TypeError"); |
| |
| test(() => { |
| smokeTest(); |
| for (const invalidAmount of invalidAmounts) { |
| const invalidDetails = { |
| total: defaultAmount, |
| displayItems: [ |
| { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: invalidAmount, |
| }, |
| }, |
| ], |
| }; |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, invalidDetails); |
| }, |
| `Expected TypeError when item.amount.value is "${invalidAmount}"` |
| ); |
| } |
| }, `For each item in details.displayItems: if item.amount.value is not a valid decimal monetary value, then throw a TypeError`); |
| |
| test(() => { |
| smokeTest(); |
| try { |
| new PaymentRequest( |
| [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| }, |
| ], |
| { |
| total: defaultTotal, |
| displayItems: [ |
| { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: "-1000", |
| }, |
| }, |
| { |
| label: "", |
| amount: { |
| currency: "AUD", |
| value: "-2000.00", |
| }, |
| }, |
| ], |
| } |
| ); |
| } catch (err) { |
| assert_unreached( |
| `shouldn't throw when given a negative value: ${err.message}` |
| ); |
| } |
| }, "Negative values are allowed for displayItems.amount.value, irrespective of total amount"); |
| |
| test(() => { |
| smokeTest(); |
| const largeMoney = "1".repeat(510); |
| try { |
| new PaymentRequest(defaultMethods, { |
| total: { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: `${largeMoney}.${largeMoney}`, |
| }, |
| }, |
| displayItems: [ |
| { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: `-${largeMoney}`, |
| }, |
| }, |
| { |
| label: "", |
| amount: { |
| currency: "AUD", |
| value: `-${largeMoney}.${largeMoney}`, |
| }, |
| }, |
| ], |
| }); |
| } catch (err) { |
| assert_unreached( |
| `shouldn't throw when given absurd monetary values: ${err.message}` |
| ); |
| } |
| }, "it handles high precision currency values without throwing"); |
| |
| // Process shipping options: |
| |
| const defaultShippingOption = Object.freeze({ |
| id: "default", |
| label: "", |
| amount: defaultAmount, |
| selected: false, |
| }); |
| const defaultShippingOptions = Object.freeze([ |
| Object.assign({}, defaultShippingOption), |
| ]); |
| |
| test(() => { |
| smokeTest(); |
| for (const amount of invalidAmounts) { |
| const invalidAmount = Object.assign({}, defaultAmount, { |
| value: amount, |
| }); |
| const invalidShippingOption = Object.assign({}, defaultShippingOption, { |
| amount: invalidAmount, |
| }); |
| const details = Object.assign({}, defaultDetails, { |
| shippingOptions: [invalidShippingOption], |
| }); |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, details, { requestShipping: true }); |
| }, |
| `Expected TypeError for option.amount.value: "${amount}"` |
| ); |
| } |
| }, `For each option in details.shippingOptions: if option.amount.value is not a valid decimal monetary value, then throw a TypeError`); |
| |
| test(() => { |
| smokeTest(); |
| const shippingOptions = [defaultShippingOption]; |
| const details = Object.assign({}, defaultDetails, { shippingOptions }); |
| const request = new PaymentRequest(defaultMethods, details); |
| assert_equals( |
| request.shippingOption, |
| null, |
| "shippingOption must be null, as requestShipping is missing" |
| ); |
| // defaultDetails lacks shipping options |
| const request2 = new PaymentRequest(defaultMethods, defaultDetails, { |
| requestShipping: true, |
| }); |
| assert_equals( |
| request2.shippingOption, |
| null, |
| `request2.shippingOption must be null` |
| ); |
| }, "If there is no selected shipping option, then PaymentRequest.shippingOption remains null"); |
| |
| test(() => { |
| smokeTest(); |
| const selectedOption = Object.assign({}, defaultShippingOption, { |
| selected: true, |
| id: "the-id", |
| }); |
| const shippingOptions = [selectedOption]; |
| const details = Object.assign({}, defaultDetails, { shippingOptions }); |
| const requestNoShippingRequested1 = new PaymentRequest( |
| defaultMethods, |
| details |
| ); |
| assert_equals( |
| requestNoShippingRequested1.shippingOption, |
| null, |
| "Must be null when no shipping is requested (defaults to false)" |
| ); |
| const requestNoShippingRequested2 = new PaymentRequest( |
| defaultMethods, |
| details, |
| { requestShipping: false } |
| ); |
| assert_equals( |
| requestNoShippingRequested2.shippingOption, |
| null, |
| "Must be null when requestShipping is false" |
| ); |
| const requestWithShipping = new PaymentRequest(defaultMethods, details, { |
| requestShipping: "truthy value", |
| }); |
| assert_equals( |
| requestWithShipping.shippingOption, |
| "the-id", |
| "Selected option must be 'the-id'" |
| ); |
| }, "If there is a selected shipping option, and requestShipping is set, then that option becomes synchronously selected"); |
| |
| test(() => { |
| smokeTest(); |
| const failOption1 = Object.assign({}, defaultShippingOption, { |
| selected: true, |
| id: "FAIL1", |
| }); |
| const failOption2 = Object.assign({}, defaultShippingOption, { |
| selected: false, |
| id: "FAIL2", |
| }); |
| const passOption = Object.assign({}, defaultShippingOption, { |
| selected: true, |
| id: "the-id", |
| }); |
| const shippingOptions = [failOption1, failOption2, passOption]; |
| const details = Object.assign({}, defaultDetails, { shippingOptions }); |
| const requestNoShipping = new PaymentRequest(defaultMethods, details, { |
| requestShipping: false, |
| }); |
| assert_equals( |
| requestNoShipping.shippingOption, |
| null, |
| "shippingOption must be null, as requestShipping is false" |
| ); |
| const requestWithShipping = new PaymentRequest(defaultMethods, details, { |
| requestShipping: true, |
| }); |
| assert_equals( |
| requestWithShipping.shippingOption, |
| "the-id", |
| "selected option must 'the-id" |
| ); |
| }, "If requestShipping is set, and if there is a multiple selected shipping options, only the last is selected."); |
| |
| test(() => { |
| smokeTest(); |
| const selectedOption = Object.assign({}, defaultShippingOption, { |
| selected: true, |
| }); |
| const unselectedOption = Object.assign({}, defaultShippingOption, { |
| selected: false, |
| }); |
| const shippingOptions = [selectedOption, unselectedOption]; |
| const details = Object.assign({}, defaultDetails, { shippingOptions }); |
| const requestNoShipping = new PaymentRequest(defaultMethods, details); |
| assert_equals( |
| requestNoShipping.shippingOption, |
| null, |
| "shippingOption must be null, because requestShipping is false" |
| ); |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, details, { requestShipping: true }); |
| }, |
| "Expected to throw a TypeError because duplicate IDs" |
| ); |
| }, "If there are any duplicate shipping option ids, and shipping is requested, then throw a TypeError"); |
| |
| test(() => { |
| smokeTest(); |
| const dupShipping1 = Object.assign({}, defaultShippingOption, { |
| selected: true, |
| id: "DUPLICATE", |
| label: "Fail 1", |
| }); |
| const dupShipping2 = Object.assign({}, defaultShippingOption, { |
| selected: false, |
| id: "DUPLICATE", |
| label: "Fail 2", |
| }); |
| const shippingOptions = [dupShipping1, defaultShippingOption, dupShipping2]; |
| const details = Object.assign({}, defaultDetails, { shippingOptions }); |
| const requestNoShipping = new PaymentRequest(defaultMethods, details); |
| assert_equals( |
| requestNoShipping.shippingOption, |
| null, |
| "shippingOption must be null, because requestShipping is false" |
| ); |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, details, { requestShipping: true }); |
| }, |
| "Expected to throw a TypeError because duplicate IDs" |
| ); |
| }, "Throw when there are duplicate shippingOption ids, even if other values are different"); |
| |
| // Process payment details modifiers: |
| test(() => { |
| smokeTest(); |
| for (const invalidTotal of invalidTotalAmounts) { |
| const invalidModifier = { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| total: { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: invalidTotal, |
| }, |
| }, |
| }; |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, { |
| modifiers: [invalidModifier], |
| total: defaultTotal, |
| }); |
| }, |
| `Expected TypeError for modifier.total.amount.value: "${invalidTotal}"` |
| ); |
| } |
| }, `Throw TypeError if modifier.total.amount.value is not a valid decimal monetary value`); |
| |
| test(() => { |
| smokeTest(); |
| for (const invalidAmount of invalidAmounts) { |
| const invalidModifier = { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| total: defaultTotal, |
| additionalDisplayItems: [ |
| { |
| label: "", |
| amount: { |
| currency: "USD", |
| value: invalidAmount, |
| }, |
| }, |
| ], |
| }; |
| assert_throws( |
| new TypeError(), |
| () => { |
| new PaymentRequest(defaultMethods, { |
| modifiers: [invalidModifier], |
| total: defaultTotal, |
| }); |
| }, |
| `Expected TypeError when given bogus modifier.additionalDisplayItems.amount of "${invalidModifier}"` |
| ); |
| } |
| }, `If amount.value of additionalDisplayItems is not a valid decimal monetary value, then throw a TypeError`); |
| |
| test(() => { |
| smokeTest(); |
| const modifiedDetails = Object.assign({}, defaultDetails, { |
| modifiers: [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data: ["some-data"], |
| }, |
| ], |
| }); |
| try { |
| new PaymentRequest(defaultMethods, modifiedDetails); |
| } catch (err) { |
| assert_unreached( |
| `Unexpected exception thrown when given a list: ${err.message}` |
| ); |
| } |
| }, "Modifier data must be JSON-serializable object (an Array in this case)"); |
| |
| test(() => { |
| smokeTest(); |
| const modifiedDetails = Object.assign({}, defaultDetails, { |
| modifiers: [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data: { |
| some: "data", |
| }, |
| }, |
| ], |
| }); |
| try { |
| new PaymentRequest(defaultMethods, modifiedDetails); |
| } catch (err) { |
| assert_unreached( |
| `shouldn't throw when given an object value: ${err.message}` |
| ); |
| } |
| }, "Modifier data must be JSON-serializable object (an Object in this case)"); |
| |
| test(() => { |
| smokeTest(); |
| const recursiveDictionary = {}; |
| recursiveDictionary.foo = recursiveDictionary; |
| const modifiedDetails = Object.assign({}, defaultDetails, { |
| modifiers: [ |
| { |
| supportedMethods: "https://wpt.fyi/payment-request", |
| data: recursiveDictionary, |
| }, |
| ], |
| }); |
| assert_throws(new TypeError(), () => { |
| new PaymentRequest(defaultMethods, modifiedDetails); |
| }); |
| }, "Rethrow any exceptions of JSON-serializing modifier.data"); |
| |
| //Setting ShippingType attribute during construction |
| test(() => { |
| smokeTest(); |
| assert_throws(new TypeError(), () => { |
| new PaymentRequest(defaultMethods, defaultDetails, { |
| shippingType: "invalid", |
| }); |
| }); |
| }, "Shipping type should be valid"); |
| |
| test(() => { |
| smokeTest(); |
| const request = new PaymentRequest(defaultMethods, defaultDetails, {}); |
| assert_equals(request.shippingAddress, null, "must be null"); |
| }, "PaymentRequest.shippingAddress must initially be null"); |
| |
| test(() => { |
| smokeTest(); |
| const request1 = new PaymentRequest(defaultMethods, defaultDetails, {}); |
| assert_equals(request1.shippingType, null, "must be null"); |
| const request2 = new PaymentRequest(defaultMethods, defaultDetails, { |
| requestShipping: false, |
| }); |
| assert_equals(request2.shippingType, null, "must be null"); |
| }, "If options.requestShipping is not set, then request.shippingType attribute is null."); |
| |
| test(() => { |
| smokeTest(); |
| // option.shippingType defaults to 'shipping' |
| const request1 = new PaymentRequest(defaultMethods, defaultDetails, { |
| requestShipping: true, |
| }); |
| assert_equals(request1.shippingType, "shipping", "must be shipping"); |
| const request2 = new PaymentRequest(defaultMethods, defaultDetails, { |
| requestShipping: true, |
| shippingType: "delivery", |
| }); |
| assert_equals(request2.shippingType, "delivery", "must be delivery"); |
| }, "If options.requestShipping is true, request.shippingType will be options.shippingType."); |
| |
| </script> |