payment-request-constructor.https.sub.html (19819B)
1 <!DOCTYPE html> 2 <!-- Copyright © 2017 Chromium authors and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). --> 3 <meta charset="utf-8"> 4 <title>Test for PaymentRequest Constructor</title> 5 <link rel="help" href="https://w3c.github.io/browser-payment-api/#constructor"> 6 <script src="/resources/testharness.js"></script> 7 <script src="/resources/testharnessreport.js"></script> 8 <script> 9 "use strict"; 10 const testMethod = Object.freeze({ 11 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 12 }); 13 const defaultMethods = Object.freeze([testMethod]); 14 const defaultAmount = Object.freeze({ 15 currency: "USD", 16 value: "1.0", 17 }); 18 const defaultNumberAmount = Object.freeze({ 19 currency: "USD", 20 value: 1.0, 21 }); 22 const defaultTotal = Object.freeze({ 23 label: "Default Total", 24 amount: defaultAmount, 25 }); 26 const defaultNumberTotal = Object.freeze({ 27 label: "Default Number Total", 28 amount: defaultNumberAmount, 29 }); 30 const defaultDetails = Object.freeze({ 31 total: defaultTotal, 32 displayItems: [ 33 { 34 label: "Default Display Item", 35 amount: defaultAmount, 36 }, 37 ], 38 }); 39 const defaultNumberDetails = Object.freeze({ 40 total: defaultNumberTotal, 41 displayItems: [ 42 { 43 label: "Default Display Item", 44 amount: defaultNumberAmount, 45 }, 46 ], 47 }); 48 49 // Avoid false positives, this should always pass 50 function smokeTest() { 51 new PaymentRequest(defaultMethods, defaultDetails); 52 new PaymentRequest(defaultMethods, defaultNumberDetails); 53 } 54 test(() => { 55 smokeTest(); 56 const request = new PaymentRequest(defaultMethods, defaultDetails); 57 assert_true(Boolean(request.id), "must be some truthy value"); 58 }, "If details.id is missing, assign an identifier"); 59 60 test(() => { 61 smokeTest(); 62 const request1 = new PaymentRequest(defaultMethods, defaultDetails); 63 const request2 = new PaymentRequest(defaultMethods, defaultDetails); 64 assert_not_equals(request1.id, request2.id, "UA generated ID must be unique"); 65 const seen = new Set(); 66 // Let's try creating lots of requests, and make sure they are all unique 67 for (let i = 0; i < 1024; i++) { 68 const request = new PaymentRequest(defaultMethods, defaultDetails); 69 assert_false( 70 seen.has(request.id), 71 `UA generated ID must be unique, but got duplicate! (${request.id})` 72 ); 73 seen.add(request.id); 74 } 75 }, "If details.id is missing, assign a unique identifier"); 76 77 test(() => { 78 smokeTest(); 79 const newDetails = Object.assign({}, defaultDetails, { id: "test123" }); 80 const request1 = new PaymentRequest(defaultMethods, newDetails); 81 const request2 = new PaymentRequest(defaultMethods, newDetails); 82 assert_equals(request1.id, newDetails.id, `id must be ${newDetails.id}`); 83 assert_equals(request2.id, newDetails.id, `id must be ${newDetails.id}`); 84 assert_equals(request1.id, request2.id, "ids need to be the same"); 85 }, "If the same id is provided, then use it"); 86 87 test(() => { 88 smokeTest(); 89 const newDetails = Object.assign({}, defaultDetails, { 90 id: "".padStart(1024, "a"), 91 }); 92 const request = new PaymentRequest(defaultMethods, newDetails); 93 assert_equals( 94 request.id, 95 newDetails.id, 96 `id must be provided value, even if very long and contain spaces` 97 ); 98 }, "Use ids even if they are strange"); 99 100 test(() => { 101 smokeTest(); 102 const request = new PaymentRequest( 103 defaultMethods, 104 Object.assign({}, defaultDetails, { id: "foo" }) 105 ); 106 assert_equals(request.id, "foo"); 107 }, "Use provided request ID"); 108 109 test(() => { 110 smokeTest(); 111 assert_throws_js(TypeError, () => new PaymentRequest([], defaultDetails)); 112 }, "If the length of the methodData sequence is zero, then throw a TypeError"); 113 114 test(() => { 115 smokeTest(); 116 const duplicateMethods = [ 117 { 118 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 119 }, 120 { 121 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 122 }, 123 ]; 124 assert_throws_js(RangeError, () => new PaymentRequest(duplicateMethods, defaultDetails)); 125 }, "If payment method is duplicate, then throw a RangeError"); 126 127 test(() => { 128 smokeTest(); 129 const JSONSerializables = [[], { object: {} }]; 130 for (const data of JSONSerializables) { 131 try { 132 const methods = [ 133 { 134 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 135 data, 136 }, 137 ]; 138 new PaymentRequest(methods, defaultDetails); 139 } catch (err) { 140 assert_unreached( 141 `Unexpected error parsing stringifiable JSON: ${JSON.stringify( 142 data 143 )}: ${err.message}` 144 ); 145 } 146 } 147 }, "Modifier method data must be JSON-serializable object"); 148 149 test(() => { 150 smokeTest(); 151 const recursiveDictionary = {}; 152 recursiveDictionary.foo = recursiveDictionary; 153 assert_throws_js(TypeError, () => { 154 const methods = [ 155 { 156 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 157 data: recursiveDictionary, 158 }, 159 ]; 160 new PaymentRequest(methods, defaultDetails); 161 }); 162 assert_throws_js(TypeError, () => { 163 const methods = [ 164 { 165 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 166 data: "a string", 167 }, 168 ]; 169 new PaymentRequest(methods, defaultDetails); 170 }); 171 assert_throws_js( 172 TypeError, 173 () => { 174 const methods = [ 175 { 176 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 177 data: null, 178 }, 179 ]; 180 new PaymentRequest(methods, defaultDetails); 181 }, 182 "Even though null is JSON-serializable, it's not type 'Object' per ES spec" 183 ); 184 }, "Rethrow any exceptions of JSON-serializing paymentMethod.data into a string"); 185 186 // process total 187 const invalidAmounts = [ 188 "-", 189 "notdigits", 190 "ALSONOTDIGITS", 191 "10.", 192 ".99", 193 "-10.", 194 "-.99", 195 "10-", 196 "1-0", 197 "1.0.0", 198 "1/3", 199 "", 200 null, 201 " 1.0 ", 202 " 1.0 ", 203 "1.0 ", 204 "USD$1.0", 205 "$1.0", 206 { 207 toString() { 208 return " 1.0"; 209 }, 210 }, 211 ]; 212 const invalidTotalAmounts = invalidAmounts.concat([ 213 "-1", 214 "-1.0", 215 "-1.00", 216 "-1000.000", 217 -10, 218 ]); 219 test(() => { 220 smokeTest(); 221 for (const invalidAmount of invalidTotalAmounts) { 222 const invalidDetails = { 223 total: { 224 label: "", 225 amount: { 226 currency: "USD", 227 value: invalidAmount, 228 }, 229 }, 230 }; 231 assert_throws_js( 232 TypeError, 233 () => { 234 new PaymentRequest(defaultMethods, invalidDetails); 235 }, 236 `Expect TypeError when details.total.amount.value is ${invalidAmount}` 237 ); 238 } 239 }, `If details.total.amount.value is not a valid decimal monetary value, then throw a TypeError`); 240 241 test(() => { 242 smokeTest(); 243 for (const prop in ["displayItems", "shippingOptions", "modifiers"]) { 244 try { 245 const details = Object.assign({}, defaultDetails, { [prop]: [] }); 246 new PaymentRequest(defaultMethods, details); 247 assert_unreached(`PaymentDetailsBase.${prop} can be zero length`); 248 } catch (err) {} 249 } 250 }, `PaymentDetailsBase members can be 0 length`); 251 252 test(() => { 253 smokeTest(); 254 assert_throws_js(TypeError, () => { 255 new PaymentRequest(defaultMethods, { 256 total: { 257 label: "", 258 amount: { 259 currency: "USD", 260 value: "-1.00", 261 }, 262 }, 263 }); 264 }); 265 }, "If the first character of details.total.amount.value is U+002D HYPHEN-MINUS, then throw a TypeError"); 266 267 test(() => { 268 smokeTest(); 269 for (const invalidAmount of invalidAmounts) { 270 const invalidDetails = { 271 total: defaultAmount, 272 displayItems: [ 273 { 274 label: "", 275 amount: { 276 currency: "USD", 277 value: invalidAmount, 278 }, 279 }, 280 ], 281 }; 282 assert_throws_js( 283 TypeError, 284 () => { 285 new PaymentRequest(defaultMethods, invalidDetails); 286 }, 287 `Expected TypeError when item.amount.value is "${invalidAmount}"` 288 ); 289 } 290 }, `For each item in details.displayItems: if item.amount.value is not a valid decimal monetary value, then throw a TypeError`); 291 292 test(() => { 293 smokeTest(); 294 try { 295 new PaymentRequest( 296 [ 297 { 298 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 299 }, 300 ], 301 { 302 total: defaultTotal, 303 displayItems: [ 304 { 305 label: "", 306 amount: { 307 currency: "USD", 308 value: "-1000", 309 }, 310 }, 311 { 312 label: "", 313 amount: { 314 currency: "AUD", 315 value: "-2000.00", 316 }, 317 }, 318 ], 319 } 320 ); 321 } catch (err) { 322 assert_unreached( 323 `shouldn't throw when given a negative value: ${err.message}` 324 ); 325 } 326 }, "Negative values are allowed for displayItems.amount.value, irrespective of total amount"); 327 328 test(() => { 329 smokeTest(); 330 const largeMoney = "1".repeat(510); 331 try { 332 new PaymentRequest(defaultMethods, { 333 total: { 334 label: "", 335 amount: { 336 currency: "USD", 337 value: `${largeMoney}.${largeMoney}`, 338 }, 339 }, 340 displayItems: [ 341 { 342 label: "", 343 amount: { 344 currency: "USD", 345 value: `-${largeMoney}`, 346 }, 347 }, 348 { 349 label: "", 350 amount: { 351 currency: "AUD", 352 value: `-${largeMoney}.${largeMoney}`, 353 }, 354 }, 355 ], 356 }); 357 } catch (err) { 358 assert_unreached( 359 `shouldn't throw when given absurd monetary values: ${err.message}` 360 ); 361 } 362 }, "it handles high precision currency values without throwing"); 363 364 // Process shipping options: 365 366 const defaultShippingOption = Object.freeze({ 367 id: "default", 368 label: "", 369 amount: defaultAmount, 370 selected: false, 371 }); 372 const defaultShippingOptions = Object.freeze([ 373 Object.assign({}, defaultShippingOption), 374 ]); 375 376 test(() => { 377 smokeTest(); 378 for (const amount of invalidAmounts) { 379 const invalidAmount = Object.assign({}, defaultAmount, { 380 value: amount, 381 }); 382 const invalidShippingOption = Object.assign({}, defaultShippingOption, { 383 amount: invalidAmount, 384 }); 385 const details = Object.assign({}, defaultDetails, { 386 shippingOptions: [invalidShippingOption], 387 }); 388 assert_throws_js( 389 TypeError, 390 () => { 391 new PaymentRequest(defaultMethods, details, { requestShipping: true }); 392 }, 393 `Expected TypeError for option.amount.value: "${amount}"` 394 ); 395 } 396 }, `For each option in details.shippingOptions: if option.amount.value is not a valid decimal monetary value, then throw a TypeError`); 397 398 test(() => { 399 smokeTest(); 400 const shippingOptions = [defaultShippingOption]; 401 const details = Object.assign({}, defaultDetails, { shippingOptions }); 402 const request = new PaymentRequest(defaultMethods, details); 403 assert_equals( 404 request.shippingOption, 405 null, 406 "shippingOption must be null, as requestShipping is missing" 407 ); 408 // defaultDetails lacks shipping options 409 const request2 = new PaymentRequest(defaultMethods, defaultDetails, { 410 requestShipping: true, 411 }); 412 assert_equals( 413 request2.shippingOption, 414 null, 415 `request2.shippingOption must be null` 416 ); 417 }, "If there is no selected shipping option, then PaymentRequest.shippingOption remains null"); 418 419 test(() => { 420 smokeTest(); 421 const selectedOption = Object.assign({}, defaultShippingOption, { 422 selected: true, 423 id: "the-id", 424 }); 425 const shippingOptions = [selectedOption]; 426 const details = Object.assign({}, defaultDetails, { shippingOptions }); 427 const requestNoShippingRequested1 = new PaymentRequest( 428 defaultMethods, 429 details 430 ); 431 assert_equals( 432 requestNoShippingRequested1.shippingOption, 433 null, 434 "Must be null when no shipping is requested (defaults to false)" 435 ); 436 const requestNoShippingRequested2 = new PaymentRequest( 437 defaultMethods, 438 details, 439 { requestShipping: false } 440 ); 441 assert_equals( 442 requestNoShippingRequested2.shippingOption, 443 null, 444 "Must be null when requestShipping is false" 445 ); 446 const requestWithShipping = new PaymentRequest(defaultMethods, details, { 447 requestShipping: "truthy value", 448 }); 449 assert_equals( 450 requestWithShipping.shippingOption, 451 "the-id", 452 "Selected option must be 'the-id'" 453 ); 454 }, "If there is a selected shipping option, and requestShipping is set, then that option becomes synchronously selected"); 455 456 test(() => { 457 smokeTest(); 458 const failOption1 = Object.assign({}, defaultShippingOption, { 459 selected: true, 460 id: "FAIL1", 461 }); 462 const failOption2 = Object.assign({}, defaultShippingOption, { 463 selected: false, 464 id: "FAIL2", 465 }); 466 const passOption = Object.assign({}, defaultShippingOption, { 467 selected: true, 468 id: "the-id", 469 }); 470 const shippingOptions = [failOption1, failOption2, passOption]; 471 const details = Object.assign({}, defaultDetails, { shippingOptions }); 472 const requestNoShipping = new PaymentRequest(defaultMethods, details, { 473 requestShipping: false, 474 }); 475 assert_equals( 476 requestNoShipping.shippingOption, 477 null, 478 "shippingOption must be null, as requestShipping is false" 479 ); 480 const requestWithShipping = new PaymentRequest(defaultMethods, details, { 481 requestShipping: true, 482 }); 483 assert_equals( 484 requestWithShipping.shippingOption, 485 "the-id", 486 "selected option must 'the-id" 487 ); 488 }, "If requestShipping is set, and if there is a multiple selected shipping options, only the last is selected."); 489 490 test(() => { 491 smokeTest(); 492 const selectedOption = Object.assign({}, defaultShippingOption, { 493 selected: true, 494 }); 495 const unselectedOption = Object.assign({}, defaultShippingOption, { 496 selected: false, 497 }); 498 const shippingOptions = [selectedOption, unselectedOption]; 499 const details = Object.assign({}, defaultDetails, { shippingOptions }); 500 const requestNoShipping = new PaymentRequest(defaultMethods, details); 501 assert_equals( 502 requestNoShipping.shippingOption, 503 null, 504 "shippingOption must be null, because requestShipping is false" 505 ); 506 assert_throws_js( 507 TypeError, 508 () => { 509 new PaymentRequest(defaultMethods, details, { requestShipping: true }); 510 }, 511 "Expected to throw a TypeError because duplicate IDs" 512 ); 513 }, "If there are any duplicate shipping option ids, and shipping is requested, then throw a TypeError"); 514 515 test(() => { 516 smokeTest(); 517 const dupShipping1 = Object.assign({}, defaultShippingOption, { 518 selected: true, 519 id: "DUPLICATE", 520 label: "Fail 1", 521 }); 522 const dupShipping2 = Object.assign({}, defaultShippingOption, { 523 selected: false, 524 id: "DUPLICATE", 525 label: "Fail 2", 526 }); 527 const shippingOptions = [dupShipping1, defaultShippingOption, dupShipping2]; 528 const details = Object.assign({}, defaultDetails, { shippingOptions }); 529 const requestNoShipping = new PaymentRequest(defaultMethods, details); 530 assert_equals( 531 requestNoShipping.shippingOption, 532 null, 533 "shippingOption must be null, because requestShipping is false" 534 ); 535 assert_throws_js( 536 TypeError, 537 () => { 538 new PaymentRequest(defaultMethods, details, { requestShipping: true }); 539 }, 540 "Expected to throw a TypeError because duplicate IDs" 541 ); 542 }, "Throw when there are duplicate shippingOption ids, even if other values are different"); 543 544 // Process payment details modifiers: 545 test(() => { 546 smokeTest(); 547 for (const invalidTotal of invalidTotalAmounts) { 548 const invalidModifier = { 549 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 550 total: { 551 label: "", 552 amount: { 553 currency: "USD", 554 value: invalidTotal, 555 }, 556 }, 557 }; 558 assert_throws_js( 559 TypeError, 560 () => { 561 new PaymentRequest(defaultMethods, { 562 modifiers: [invalidModifier], 563 total: defaultTotal, 564 }); 565 }, 566 `Expected TypeError for modifier.total.amount.value: "${invalidTotal}"` 567 ); 568 } 569 }, `Throw TypeError if modifier.total.amount.value is not a valid decimal monetary value`); 570 571 test(() => { 572 smokeTest(); 573 for (const invalidAmount of invalidAmounts) { 574 const invalidModifier = { 575 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 576 total: defaultTotal, 577 additionalDisplayItems: [ 578 { 579 label: "", 580 amount: { 581 currency: "USD", 582 value: invalidAmount, 583 }, 584 }, 585 ], 586 }; 587 assert_throws_js( 588 TypeError, 589 () => { 590 new PaymentRequest(defaultMethods, { 591 modifiers: [invalidModifier], 592 total: defaultTotal, 593 }); 594 }, 595 `Expected TypeError when given bogus modifier.additionalDisplayItems.amount of "${invalidModifier}"` 596 ); 597 } 598 }, `If amount.value of additionalDisplayItems is not a valid decimal monetary value, then throw a TypeError`); 599 600 test(() => { 601 smokeTest(); 602 const modifiedDetails = Object.assign({}, defaultDetails, { 603 modifiers: [ 604 { 605 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 606 data: ["some-data"], 607 }, 608 ], 609 }); 610 try { 611 new PaymentRequest(defaultMethods, modifiedDetails); 612 } catch (err) { 613 assert_unreached( 614 `Unexpected exception thrown when given a list: ${err.message}` 615 ); 616 } 617 }, "Modifier data must be JSON-serializable object (an Array in this case)"); 618 619 test(() => { 620 smokeTest(); 621 const modifiedDetails = Object.assign({}, defaultDetails, { 622 modifiers: [ 623 { 624 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 625 data: { 626 some: "data", 627 }, 628 }, 629 ], 630 }); 631 try { 632 new PaymentRequest(defaultMethods, modifiedDetails); 633 } catch (err) { 634 assert_unreached( 635 `shouldn't throw when given an object value: ${err.message}` 636 ); 637 } 638 }, "Modifier data must be JSON-serializable object (an Object in this case)"); 639 640 test(() => { 641 smokeTest(); 642 const recursiveDictionary = {}; 643 recursiveDictionary.foo = recursiveDictionary; 644 const modifiedDetails = Object.assign({}, defaultDetails, { 645 modifiers: [ 646 { 647 supportedMethods: "https://{{domains[nonexistent]}}/payment-request", 648 data: recursiveDictionary, 649 }, 650 ], 651 }); 652 assert_throws_js(TypeError, () => { 653 new PaymentRequest(defaultMethods, modifiedDetails); 654 }); 655 }, "Rethrow any exceptions of JSON-serializing modifier.data"); 656 657 //Setting ShippingType attribute during construction 658 test(() => { 659 smokeTest(); 660 assert_throws_js(TypeError, () => { 661 new PaymentRequest(defaultMethods, defaultDetails, { 662 shippingType: "invalid", 663 }); 664 }); 665 }, "Shipping type should be valid"); 666 667 test(() => { 668 smokeTest(); 669 const request = new PaymentRequest(defaultMethods, defaultDetails, {}); 670 assert_equals(request.shippingAddress, null, "must be null"); 671 }, "PaymentRequest.shippingAddress must initially be null"); 672 673 test(() => { 674 smokeTest(); 675 const request1 = new PaymentRequest(defaultMethods, defaultDetails, {}); 676 assert_equals(request1.shippingType, null, "must be null"); 677 const request2 = new PaymentRequest(defaultMethods, defaultDetails, { 678 requestShipping: false, 679 }); 680 assert_equals(request2.shippingType, null, "must be null"); 681 }, "If options.requestShipping is not set, then request.shippingType attribute is null."); 682 683 test(() => { 684 smokeTest(); 685 // option.shippingType defaults to 'shipping' 686 const request1 = new PaymentRequest(defaultMethods, defaultDetails, { 687 requestShipping: true, 688 }); 689 assert_equals(request1.shippingType, "shipping", "must be shipping"); 690 const request2 = new PaymentRequest(defaultMethods, defaultDetails, { 691 requestShipping: true, 692 shippingType: "delivery", 693 }); 694 assert_equals(request2.shippingType, "delivery", "must be delivery"); 695 }, "If options.requestShipping is true, request.shippingType will be options.shippingType."); 696 697 </script>