[Payment Request] Update web platform tests
[WebKit-https.git] / LayoutTests / imported / w3c / web-platform-tests / payment-request / payment-request-constructor.https.html
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://wpt.fyi/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(new 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 JSONSerializables = [[], { object: {} }];
117   for (const data of JSONSerializables) {
118     try {
119       const methods = [
120         {
121           supportedMethods: "https://wpt.fyi/payment-request",
122           data,
123         },
124       ];
125       new PaymentRequest(methods, defaultDetails);
126     } catch (err) {
127       assert_unreached(
128         `Unexpected error parsing stringifiable JSON: ${JSON.stringify(
129           data
130         )}: ${err.message}`
131       );
132     }
133   }
134 }, "Modifier method data must be JSON-serializable object");
135
136 test(() => {
137   smokeTest();
138   const recursiveDictionary = {};
139   recursiveDictionary.foo = recursiveDictionary;
140   assert_throws(new TypeError(), () => {
141     const methods = [
142       {
143         supportedMethods: "https://wpt.fyi/payment-request",
144         data: recursiveDictionary,
145       },
146     ];
147     new PaymentRequest(methods, defaultDetails);
148   });
149   assert_throws(new TypeError(), () => {
150     const methods = [
151       {
152         supportedMethods: "https://wpt.fyi/payment-request",
153         data: "a string",
154       },
155     ];
156     new PaymentRequest(methods, defaultDetails);
157   });
158   assert_throws(
159     new TypeError(),
160     () => {
161       const methods = [
162         {
163           supportedMethods: "https://wpt.fyi/payment-request",
164           data: null,
165         },
166       ];
167       new PaymentRequest(methods, defaultDetails);
168     },
169     "Even though null is JSON-serializable, it's not type 'Object' per ES spec"
170   );
171 }, "Rethrow any exceptions of JSON-serializing paymentMethod.data into a string");
172
173 // process total
174 const invalidAmounts = [
175   "-",
176   "notdigits",
177   "ALSONOTDIGITS",
178   "10.",
179   ".99",
180   "-10.",
181   "-.99",
182   "10-",
183   "1-0",
184   "1.0.0",
185   "1/3",
186   "",
187   null,
188   " 1.0  ",
189   " 1.0 ",
190   "1.0 ",
191   "USD$1.0",
192   "$1.0",
193   {
194     toString() {
195       return " 1.0";
196     },
197   },
198 ];
199 const invalidTotalAmounts = invalidAmounts.concat([
200   "-1",
201   "-1.0",
202   "-1.00",
203   "-1000.000",
204   -10,
205 ]);
206 test(() => {
207   smokeTest();
208   for (const invalidAmount of invalidTotalAmounts) {
209     const invalidDetails = {
210       total: {
211         label: "",
212         amount: {
213           currency: "USD",
214           value: invalidAmount,
215         },
216       },
217     };
218     assert_throws(
219       new TypeError(),
220       () => {
221         new PaymentRequest(defaultMethods, invalidDetails);
222       },
223       `Expect TypeError when details.total.amount.value is ${invalidAmount}`
224     );
225   }
226 }, `If details.total.amount.value is not a valid decimal monetary value, then throw a TypeError`);
227
228 test(() => {
229   smokeTest();
230   for (const prop in ["displayItems", "shippingOptions", "modifiers"]) {
231     try {
232       const details = Object.assign({}, defaultDetails, { [prop]: [] });
233       new PaymentRequest(defaultMethods, details);
234       assert_unreached(`PaymentDetailsBase.${prop} can be zero length`);
235     } catch (err) {}
236   }
237 }, `PaymentDetailsBase members can be 0 length`);
238
239 test(() => {
240   smokeTest();
241   assert_throws(new TypeError(), () => {
242     new PaymentRequest(defaultMethods, {
243       total: {
244         label: "",
245         amount: {
246           currency: "USD",
247           value: "-1.00",
248         },
249       },
250     });
251   });
252 }, "If the first character of details.total.amount.value is U+002D HYPHEN-MINUS, then throw a TypeError");
253
254 test(() => {
255   smokeTest();
256   for (const invalidAmount of invalidAmounts) {
257     const invalidDetails = {
258       total: defaultAmount,
259       displayItems: [
260         {
261           label: "",
262           amount: {
263             currency: "USD",
264             value: invalidAmount,
265           },
266         },
267       ],
268     };
269     assert_throws(
270       new TypeError(),
271       () => {
272         new PaymentRequest(defaultMethods, invalidDetails);
273       },
274       `Expected TypeError when item.amount.value is "${invalidAmount}"`
275     );
276   }
277 }, `For each item in details.displayItems: if item.amount.value is not a valid decimal monetary value, then throw a TypeError`);
278
279 test(() => {
280   smokeTest();
281   try {
282     new PaymentRequest(
283       [
284         {
285           supportedMethods: "https://wpt.fyi/payment-request",
286         },
287       ],
288       {
289         total: defaultTotal,
290         displayItems: [
291           {
292             label: "",
293             amount: {
294               currency: "USD",
295               value: "-1000",
296             },
297           },
298           {
299             label: "",
300             amount: {
301               currency: "AUD",
302               value: "-2000.00",
303             },
304           },
305         ],
306       }
307     );
308   } catch (err) {
309     assert_unreached(
310       `shouldn't throw when given a negative value: ${err.message}`
311     );
312   }
313 }, "Negative values are allowed for displayItems.amount.value, irrespective of total amount");
314
315 test(() => {
316   smokeTest();
317   const largeMoney = "1".repeat(510);
318   try {
319     new PaymentRequest(defaultMethods, {
320       total: {
321         label: "",
322         amount: {
323           currency: "USD",
324           value: `${largeMoney}.${largeMoney}`,
325         },
326       },
327       displayItems: [
328         {
329           label: "",
330           amount: {
331             currency: "USD",
332             value: `-${largeMoney}`,
333           },
334         },
335         {
336           label: "",
337           amount: {
338             currency: "AUD",
339             value: `-${largeMoney}.${largeMoney}`,
340           },
341         },
342       ],
343     });
344   } catch (err) {
345     assert_unreached(
346       `shouldn't throw when given absurd monetary values: ${err.message}`
347     );
348   }
349 }, "it handles high precision currency values without throwing");
350
351 // Process shipping options:
352
353 const defaultShippingOption = Object.freeze({
354   id: "default",
355   label: "",
356   amount: defaultAmount,
357   selected: false,
358 });
359 const defaultShippingOptions = Object.freeze([
360   Object.assign({}, defaultShippingOption),
361 ]);
362
363 test(() => {
364   smokeTest();
365   for (const amount of invalidAmounts) {
366     const invalidAmount = Object.assign({}, defaultAmount, {
367       value: amount,
368     });
369     const invalidShippingOption = Object.assign({}, defaultShippingOption, {
370       amount: invalidAmount,
371     });
372     const details = Object.assign({}, defaultDetails, {
373       shippingOptions: [invalidShippingOption],
374     });
375     assert_throws(
376       new TypeError(),
377       () => {
378         new PaymentRequest(defaultMethods, details, { requestShipping: true });
379       },
380       `Expected TypeError for option.amount.value: "${amount}"`
381     );
382   }
383 }, `For each option in details.shippingOptions: if option.amount.value is not a valid decimal monetary value, then throw a TypeError`);
384
385 test(() => {
386   smokeTest();
387   const shippingOptions = [defaultShippingOption];
388   const details = Object.assign({}, defaultDetails, { shippingOptions });
389   const request = new PaymentRequest(defaultMethods, details);
390   assert_equals(
391     request.shippingOption,
392     null,
393     "shippingOption must be null, as requestShipping is missing"
394   );
395   // defaultDetails lacks shipping options
396   const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
397     requestShipping: true,
398   });
399   assert_equals(
400     request2.shippingOption,
401     null,
402     `request2.shippingOption must be null`
403   );
404 }, "If there is no selected shipping option, then PaymentRequest.shippingOption remains null");
405
406 test(() => {
407   smokeTest();
408   const selectedOption = Object.assign({}, defaultShippingOption, {
409     selected: true,
410     id: "the-id",
411   });
412   const shippingOptions = [selectedOption];
413   const details = Object.assign({}, defaultDetails, { shippingOptions });
414   const requestNoShippingRequested1 = new PaymentRequest(
415     defaultMethods,
416     details
417   );
418   assert_equals(
419     requestNoShippingRequested1.shippingOption,
420     null,
421     "Must be null when no shipping is requested (defaults to false)"
422   );
423   const requestNoShippingRequested2 = new PaymentRequest(
424     defaultMethods,
425     details,
426     { requestShipping: false }
427   );
428   assert_equals(
429     requestNoShippingRequested2.shippingOption,
430     null,
431     "Must be null when requestShipping is false"
432   );
433   const requestWithShipping = new PaymentRequest(defaultMethods, details, {
434     requestShipping: "truthy value",
435   });
436   assert_equals(
437     requestWithShipping.shippingOption,
438     "the-id",
439     "Selected option must be 'the-id'"
440   );
441 }, "If there is a selected shipping option, and requestShipping is set, then that option becomes synchronously selected");
442
443 test(() => {
444   smokeTest();
445   const failOption1 = Object.assign({}, defaultShippingOption, {
446     selected: true,
447     id: "FAIL1",
448   });
449   const failOption2 = Object.assign({}, defaultShippingOption, {
450     selected: false,
451     id: "FAIL2",
452   });
453   const passOption = Object.assign({}, defaultShippingOption, {
454     selected: true,
455     id: "the-id",
456   });
457   const shippingOptions = [failOption1, failOption2, passOption];
458   const details = Object.assign({}, defaultDetails, { shippingOptions });
459   const requestNoShipping = new PaymentRequest(defaultMethods, details, {
460     requestShipping: false,
461   });
462   assert_equals(
463     requestNoShipping.shippingOption,
464     null,
465     "shippingOption must be null, as requestShipping is false"
466   );
467   const requestWithShipping = new PaymentRequest(defaultMethods, details, {
468     requestShipping: true,
469   });
470   assert_equals(
471     requestWithShipping.shippingOption,
472     "the-id",
473     "selected option must 'the-id"
474   );
475 }, "If requestShipping is set, and if there is a multiple selected shipping options, only the last is selected.");
476
477 test(() => {
478   smokeTest();
479   const selectedOption = Object.assign({}, defaultShippingOption, {
480     selected: true,
481   });
482   const unselectedOption = Object.assign({}, defaultShippingOption, {
483     selected: false,
484   });
485   const shippingOptions = [selectedOption, unselectedOption];
486   const details = Object.assign({}, defaultDetails, { shippingOptions });
487   const requestNoShipping = new PaymentRequest(defaultMethods, details);
488   assert_equals(
489     requestNoShipping.shippingOption,
490     null,
491     "shippingOption must be null, because requestShipping is false"
492   );
493   assert_throws(
494     new TypeError(),
495     () => {
496       new PaymentRequest(defaultMethods, details, { requestShipping: true });
497     },
498     "Expected to throw a TypeError because duplicate IDs"
499   );
500 }, "If there are any duplicate shipping option ids, and shipping is requested, then throw a TypeError");
501
502 test(() => {
503   smokeTest();
504   const dupShipping1 = Object.assign({}, defaultShippingOption, {
505     selected: true,
506     id: "DUPLICATE",
507     label: "Fail 1",
508   });
509   const dupShipping2 = Object.assign({}, defaultShippingOption, {
510     selected: false,
511     id: "DUPLICATE",
512     label: "Fail 2",
513   });
514   const shippingOptions = [dupShipping1, defaultShippingOption, dupShipping2];
515   const details = Object.assign({}, defaultDetails, { shippingOptions });
516   const requestNoShipping = new PaymentRequest(defaultMethods, details);
517   assert_equals(
518     requestNoShipping.shippingOption,
519     null,
520     "shippingOption must be null, because requestShipping is false"
521   );
522   assert_throws(
523     new TypeError(),
524     () => {
525       new PaymentRequest(defaultMethods, details, { requestShipping: true });
526     },
527     "Expected to throw a TypeError because duplicate IDs"
528   );
529 }, "Throw when there are duplicate shippingOption ids, even if other values are different");
530
531 // Process payment details modifiers:
532 test(() => {
533   smokeTest();
534   for (const invalidTotal of invalidTotalAmounts) {
535     const invalidModifier = {
536       supportedMethods: "https://wpt.fyi/payment-request",
537       total: {
538         label: "",
539         amount: {
540           currency: "USD",
541           value: invalidTotal,
542         },
543       },
544     };
545     assert_throws(
546       new TypeError(),
547       () => {
548         new PaymentRequest(defaultMethods, {
549           modifiers: [invalidModifier],
550           total: defaultTotal,
551         });
552       },
553       `Expected TypeError for modifier.total.amount.value: "${invalidTotal}"`
554     );
555   }
556 }, `Throw TypeError if modifier.total.amount.value is not a valid decimal monetary value`);
557
558 test(() => {
559   smokeTest();
560   for (const invalidAmount of invalidAmounts) {
561     const invalidModifier = {
562       supportedMethods: "https://wpt.fyi/payment-request",
563       total: defaultTotal,
564       additionalDisplayItems: [
565         {
566           label: "",
567           amount: {
568             currency: "USD",
569             value: invalidAmount,
570           },
571         },
572       ],
573     };
574     assert_throws(
575       new TypeError(),
576       () => {
577         new PaymentRequest(defaultMethods, {
578           modifiers: [invalidModifier],
579           total: defaultTotal,
580         });
581       },
582       `Expected TypeError when given bogus modifier.additionalDisplayItems.amount of "${invalidModifier}"`
583     );
584   }
585 }, `If amount.value of additionalDisplayItems is not a valid decimal monetary value, then throw a TypeError`);
586
587 test(() => {
588   smokeTest();
589   const modifiedDetails = Object.assign({}, defaultDetails, {
590     modifiers: [
591       {
592         supportedMethods: "https://wpt.fyi/payment-request",
593         data: ["some-data"],
594       },
595     ],
596   });
597   try {
598     new PaymentRequest(defaultMethods, modifiedDetails);
599   } catch (err) {
600     assert_unreached(
601       `Unexpected exception thrown when given a list: ${err.message}`
602     );
603   }
604 }, "Modifier data must be JSON-serializable object (an Array in this case)");
605
606 test(() => {
607   smokeTest();
608   const modifiedDetails = Object.assign({}, defaultDetails, {
609     modifiers: [
610       {
611         supportedMethods: "https://wpt.fyi/payment-request",
612         data: {
613           some: "data",
614         },
615       },
616     ],
617   });
618   try {
619     new PaymentRequest(defaultMethods, modifiedDetails);
620   } catch (err) {
621     assert_unreached(
622       `shouldn't throw when given an object value: ${err.message}`
623     );
624   }
625 }, "Modifier data must be JSON-serializable object (an Object in this case)");
626
627 test(() => {
628   smokeTest();
629   const recursiveDictionary = {};
630   recursiveDictionary.foo = recursiveDictionary;
631   const modifiedDetails = Object.assign({}, defaultDetails, {
632     modifiers: [
633       {
634         supportedMethods: "https://wpt.fyi/payment-request",
635         data: recursiveDictionary,
636       },
637     ],
638   });
639   assert_throws(new TypeError(), () => {
640     new PaymentRequest(defaultMethods, modifiedDetails);
641   });
642 }, "Rethrow any exceptions of JSON-serializing modifier.data");
643
644 //Setting ShippingType attribute during construction
645 test(() => {
646   smokeTest();
647   assert_throws(new TypeError(), () => {
648     new PaymentRequest(defaultMethods, defaultDetails, {
649       shippingType: "invalid",
650     });
651   });
652 }, "Shipping type should be valid");
653
654 test(() => {
655   smokeTest();
656   const request = new PaymentRequest(defaultMethods, defaultDetails, {});
657   assert_equals(request.shippingAddress, null, "must be null");
658 }, "PaymentRequest.shippingAddress must initially be null");
659
660 test(() => {
661   smokeTest();
662   const request1 = new PaymentRequest(defaultMethods, defaultDetails, {});
663   assert_equals(request1.shippingType, null, "must be null");
664   const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
665     requestShipping: false,
666   });
667   assert_equals(request2.shippingType, null, "must be null");
668 }, "If options.requestShipping is not set, then request.shippingType attribute is null.");
669
670 test(() => {
671   smokeTest();
672   // option.shippingType defaults to 'shipping'
673   const request1 = new PaymentRequest(defaultMethods, defaultDetails, {
674     requestShipping: true,
675   });
676   assert_equals(request1.shippingType, "shipping", "must be shipping");
677   const request2 = new PaymentRequest(defaultMethods, defaultDetails, {
678     requestShipping: true,
679     shippingType: "delivery",
680   });
681   assert_equals(request2.shippingType, "delivery", "must be delivery");
682 }, "If options.requestShipping is true, request.shippingType will be options.shippingType.");
683
684 </script>