| <!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://{{domains[nonexistent]}}/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_js(TypeError, () => new PaymentRequest([], defaultDetails)); | |
| }, "If the length of the methodData sequence is zero, then throw a TypeError"); | |
| test(() => { | |
| smokeTest(); | |
| const duplicateMethods = [ | |
| { | |
| supportedMethods: "https://{{domains[nonexistent]}}/payment-request", | |
| }, | |
| { | |
| supportedMethods: "https://{{domains[nonexistent]}}/payment-request", | |
| }, | |
| ]; | |
| assert_throws_js(RangeError, () => new PaymentRequest(duplicateMethods, defaultDetails)); | |
| }, "If payment method is duplicate, then throw a RangeError"); | |
| test(() => { | |
| smokeTest(); | |
| const JSONSerializables = [[], { object: {} }]; | |
| for (const data of JSONSerializables) { | |
| try { | |
| const methods = [ | |
| { | |
| supportedMethods: "https://{{domains[nonexistent]}}/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_js(TypeError, () => { | |
| const methods = [ | |
| { | |
| supportedMethods: "https://{{domains[nonexistent]}}/payment-request", | |
| data: recursiveDictionary, | |
| }, | |
| ]; | |
| new PaymentRequest(methods, defaultDetails); | |
| }); | |
| assert_throws_js(TypeError, () => { | |
| const methods = [ | |
| { | |
| supportedMethods: "https://{{domains[nonexistent]}}/payment-request", | |
| data: "a string", | |
| }, | |
| ]; | |
| new PaymentRequest(methods, defaultDetails); | |
| }); | |
| assert_throws_js( | |
| TypeError, | |
| () => { | |
| const methods = [ | |
| { | |
| supportedMethods: "https://{{domains[nonexistent]}}/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_js( | |
| 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", "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_js(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_js( | |
| 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://{{domains[nonexistent]}}/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 payment details modifiers: | |
| test(() => { | |
| smokeTest(); | |
| for (const invalidTotal of invalidTotalAmounts) { | |
| const invalidModifier = { | |
| supportedMethods: "https://{{domains[nonexistent]}}/payment-request", | |
| total: { | |
| label: "", | |
| amount: { | |
| currency: "USD", | |
| value: invalidTotal, | |
| }, | |
| }, | |
| }; | |
| assert_throws_js( | |
| 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://{{domains[nonexistent]}}/payment-request", | |
| total: defaultTotal, | |
| additionalDisplayItems: [ | |
| { | |
| label: "", | |
| amount: { | |
| currency: "USD", | |
| value: invalidAmount, | |
| }, | |
| }, | |
| ], | |
| }; | |
| assert_throws_js( | |
| 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://{{domains[nonexistent]}}/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://{{domains[nonexistent]}}/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://{{domains[nonexistent]}}/payment-request", | |
| data: recursiveDictionary, | |
| }, | |
| ], | |
| }); | |
| assert_throws_js(TypeError, () => { | |
| new PaymentRequest(defaultMethods, modifiedDetails); | |
| }); | |
| }, "Rethrow any exceptions of JSON-serializing modifier.data"); | |
| </script> |