| <!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", | |
| data: { | |
| supportedTypes: ["debit"], | |
| }, | |
| }, | |
| ], | |
| { | |
| 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> |