summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/payment-request
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/payment-request
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/payment-request')
-rw-r--r--testing/web-platform/tests/payment-request/META.yml7
-rw-r--r--testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodDetails-attribute.https.html32
-rw-r--r--testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodName-attribute.https.html28
-rw-r--r--testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.http.html12
-rw-r--r--testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.https.html52
-rw-r--r--testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/updatewith-method.https.html62
-rw-r--r--testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-error-member-manual.https.html50
-rw-r--r--testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-payer-member-manual.https.html65
-rw-r--r--testing/web-platform/tests/payment-request/constructor_convert_method_data.https.html73
-rw-r--r--testing/web-platform/tests/payment-request/delegate-request.https.sub.html81
-rw-r--r--testing/web-platform/tests/payment-request/historical.https.html45
-rw-r--r--testing/web-platform/tests/payment-request/idlharness.https.window.js31
-rw-r--r--testing/web-platform/tests/payment-request/onpaymentmethodchange-attribute.https.html79
-rw-r--r--testing/web-platform/tests/payment-request/payment-is-showing.https.html170
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-abort-method.https.html91
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-canmakepayment-method.https.html120
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-constructor.https.sub.html477
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-ctor-currency-code-checks.https.sub.html282
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-ctor-pmi-handling.https.sub.html149
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-disallowed-when-hidden.https.html48
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-manual.tentative.https.html95
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-protection.tentative.https.html68
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method.tentative.https.html78
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-id-attribute.https.html39
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-insecure.http.html13
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-not-exposed.https.worker.js7
-rw-r--r--testing/web-platform/tests/payment-request/payment-request-show-method.https.html105
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/complete-method-manual.https.html101
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/helpers.js110
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/methodName-attribute-manual.https.html28
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute-manual.https.html73
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute.https.html14
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/payerEmail-attribute-manual.https.html48
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/payerName-attribute-manual.https.html48
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/payerPhone-attribute-manual.https.html48
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-immediate-manual.https.html68
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-manual.https.html56
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/rejects_if_not_active-manual.https.html160
-rw-r--r--testing/web-platform/tests/payment-request/payment-response/requestId-attribute-manual.https.html34
-rw-r--r--testing/web-platform/tests/payment-request/rejects_if_not_active.https.html142
-rw-r--r--testing/web-platform/tests/payment-request/resources/delegate-request-subframe.sub.html38
-rw-r--r--testing/web-platform/tests/payment-request/resources/page1.html1
-rw-r--r--testing/web-platform/tests/payment-request/resources/page2.html1
-rw-r--r--testing/web-platform/tests/payment-request/show-consume-activation.https.html52
-rw-r--r--testing/web-platform/tests/payment-request/show-method-optional-promise-rejects.https.html215
-rw-r--r--testing/web-platform/tests/payment-request/show-method-postmessage-iframe.html49
-rw-r--r--testing/web-platform/tests/payment-request/show-method-postmessage-manual.https.html53
-rw-r--r--testing/web-platform/tests/payment-request/user-abort-algorithm-manual.https.html80
48 files changed, 3778 insertions, 0 deletions
diff --git a/testing/web-platform/tests/payment-request/META.yml b/testing/web-platform/tests/payment-request/META.yml
new file mode 100644
index 0000000000..1dbe3e5d7e
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/META.yml
@@ -0,0 +1,7 @@
+spec: https://w3c.github.io/payment-request/
+suggested_reviewers:
+ - marcoscaceres
+ - rsolomakhin
+ - zouhir
+ - romandev
+ - aestes
diff --git a/testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodDetails-attribute.https.html b/testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodDetails-attribute.https.html
new file mode 100644
index 0000000000..feaaef66ad
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodDetails-attribute.https.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for PaymentMethodChangeEvent.methodDetails attribute</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#dom-paymentmethodchangeevent-methoddetails">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+test(() => {
+ const methodDetails = {
+ test: "pass"
+ }
+ const event = new PaymentMethodChangeEvent("test", {
+ methodName: "wpt-test",
+ methodDetails
+ });
+ assert_idl_attribute(event, "methodDetails");
+ const { test } = event.methodDetails;
+ assert_equals(test, "pass");
+}, "Must have a methodDetails IDL attribute, which is initialized with to the methodName dictionary value");
+
+test(() => {
+ const event = new PaymentMethodChangeEvent("test");
+ assert_equals(event.methodDetails, null, "methodDetails attribute must initialize to null");
+
+ const event2 = new PaymentMethodChangeEvent("test", { methodName: "basic-card" });
+ assert_equals(event2.methodDetails, null, "methodDetails attribute must initialize to null");
+
+ const event3 = new PaymentMethodChangeEvent("test", {});
+ assert_equals(event2.methodDetails, null, "methodDetails attribute must initialize to null");
+}, "The methodDetails member defaults to null");
+</script>
diff --git a/testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodName-attribute.https.html b/testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodName-attribute.https.html
new file mode 100644
index 0000000000..176638c785
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentMethodChangeEvent/methodName-attribute.https.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for PaymentMethodChangeEvent.methodName attribute</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#dom-paymentmethodchangeevent-src">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+test(() => {
+ const event = new PaymentMethodChangeEvent("test", {
+ methodName: "wpt-test",
+ });
+ assert_idl_attribute(event, "methodName");
+ const { methodName } = event;
+ assert_equals(methodName, "wpt-test");
+}, "Must have a methodName IDL attribute, which is initialized with to the methodName dictionary value");
+
+test(() => {
+ const event = new PaymentMethodChangeEvent("test");
+ assert_equals(event.methodName, "", "methodName attribute must initialize to empty string");
+
+ const event2 = new PaymentMethodChangeEvent("test", { methodDetails: {} });
+ assert_equals(event2.methodName, "", "methodName attribute must initialize to empty string");
+
+ const event3 = new PaymentMethodChangeEvent("test", {});
+ assert_equals(event3.methodName, "", "methodName attribute must initialize to empty string");
+}, "When no dictionary is passed, the methodName member defaults to the empty string");
+</script>
diff --git a/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.http.html b/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.http.html
new file mode 100644
index 0000000000..db7765f7bf
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.http.html
@@ -0,0 +1,12 @@
+<!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 PaymentRequestUpdateEvent Constructor (insecure)</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#paymentrequestupdateevent-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_false("PaymentRequestUpdateEvent" in Window);
+}, "PaymentRequestUpdateEvent constructor must not be exposed in insecure context");
+</script>
diff --git a/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.https.html b/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.https.html
new file mode 100644
index 0000000000..3de0469e9c
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/constructor.https.html
@@ -0,0 +1,52 @@
+<!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 PaymentRequestUpdateEvent 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>
+const examplePay = Object.freeze({ supportedMethods: "https://example.com/pay" });
+const defaultMethods = Object.freeze([examplePay]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+test(() => {
+ try {
+ new PaymentRequestUpdateEvent("test");
+ } catch (err) {
+ assert_unreached(`Unexpected exception: ${err.message}`);
+ }
+}, "PaymentRequestUpdateEvent can be constructed in secure-context");
+
+test(() => {
+ const ev = new PaymentRequestUpdateEvent("test", {
+ bubbles: true,
+ cancelable: true,
+ composed: true,
+ });
+ assert_false(ev.isTrusted, "constructed in script, so not be trusted");
+ assert_true(ev.bubbles, "set by EventInitDict");
+ assert_true(ev.cancelable, "set by EventInitDict");
+ assert_true(ev.composed, "set by EventInitDict");
+ assert_equals(ev.target, null, "initially null");
+ assert_equals(ev.type, "test");
+}, "PaymentRequestUpdateEvent can be constructed with an EventInitDict, even if not trusted");
+
+test(() => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const ev = new PaymentRequestUpdateEvent("test");
+ request.addEventListener("test", evt => {
+ assert_equals(ev, evt);
+ });
+ request.dispatchEvent(ev);
+}, "PaymentRequestUpdateEvent can be dispatched, even if not trusted");
+
+</script>
diff --git a/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/updatewith-method.https.html b/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/updatewith-method.https.html
new file mode 100644
index 0000000000..9a60fe7a4c
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentRequestUpdateEvent/updatewith-method.https.html
@@ -0,0 +1,62 @@
+<!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 PaymentRequestUpdateEvent's updateWith() method</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#updatewith-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const examplePay = Object.freeze({ supportedMethods: "https://example.com/pay" });
+const defaultMethods = Object.freeze([examplePay]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+test(() => {
+ // Smoke test - checks target is set as expected
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const ev = new PaymentRequestUpdateEvent("test");
+ request.dispatchEvent(ev);
+ assert_equals(ev.target, request, "The request and the target at the same");
+}, "Let target be the request which is dispatching the event.");
+
+// Github issue: https://github.com/w3c/browser-payment-api/issues/546
+test(() => {
+ const untrustedEvents = [
+ new PaymentRequestUpdateEvent("just a test")
+ ].forEach(ev => {
+ assert_throws_dom(
+ "InvalidStateError",
+ () => {
+ ev.updateWith(Promise.resolve());
+ },
+ `untrusted event of type "${ev.type}" must throw "InvalidStateError"`
+ );
+ });
+}, `Calling .updateWith() with an undispatched untrusted event throws "InvalidStateError"`);
+
+// Github issue: https://github.com/w3c/browser-payment-api/issues/546
+test(() => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const untrustedEvents = [
+ new PaymentRequestUpdateEvent("just a test")
+ ].map(ev => {
+ request.dispatchEvent(ev); // set .target and dispatch flag
+ // unstrusted event.
+ assert_throws_dom(
+ "InvalidStateError",
+ () => {
+ ev.updateWith(Promise.resolve())
+ },
+ `untrusted event of type "${ev.type}" must throw "InvalidStateError"`
+ );
+ });
+}, `Calling .updateWith() with a dispatched, untrusted event, throws "InvalidStateError"`);
+
+</script>
diff --git a/testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-error-member-manual.https.html b/testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-error-member-manual.https.html
new file mode 100644
index 0000000000..9135520cd7
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-error-member-manual.https.html
@@ -0,0 +1,50 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentvalidationerrors-error">
+<title>
+ PaymentValidationErrors' `error` member
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../payment-response/helpers.js"></script>
+<script>
+function retryShowsErrorMember(button) {
+ button.disabled = true;
+ promise_test(async t => {
+ const { response } = await getPaymentRequestResponse();
+ await response.retry({ error: "PASS" });
+ await response.complete("success");
+ }, button.textContent.trim());
+}
+</script>
+<h2>
+ Manual Test for PaymentValidationErrors's `error` member - Please run in order!
+</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When presented with the payment sheet, use any card and select to "Pay".
+ You will be asked to retry the payment and an error should be shown somewhere
+ in the UI. The expected error string is described in each individual test.
+ If you see the error, hit "Pay" again. If you don't see the error,
+ abort the payment request by hitting "esc" - which means that particular test
+ has failed.
+</p>
+<ol>
+ <li>
+ <button onclick="retryShowsErrorMember(this);">
+ The payment sheet shows the error "PASS" somewhere in the UI.
+ </button>
+ </li>
+ <li>
+ <button onclick="done();">
+ Done!
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">owners</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-payer-member-manual.https.html b/testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-payer-member-manual.https.html
new file mode 100644
index 0000000000..f8115e69ec
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/PaymentValidationErrors/retry-shows-payer-member-manual.https.html
@@ -0,0 +1,65 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentvalidationerrors-payer">
+<title>
+ PaymentValidationErrors' `payer` member
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../payment-response/helpers.js"></script>
+<script>
+function retryShowsPayerMember(button, error) {
+ button.disabled = true;
+ promise_test(async t => {
+ const options = {
+ requestPayerName: true,
+ requestPayerEmail: true,
+ requestPayerPhone: true,
+ }
+ const { response } = await getPaymentRequestResponse(options);
+ await response.retry({ payer: error });
+ await response.complete("success");
+ }, button.textContent.trim());
+}
+</script>
+<h2>
+ Manual Test for PaymentValidationErrors' `payer` member - Please run in order!
+</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When presented with the payment sheet, use any card and select to "Pay".
+ You will be asked to retry the payment and an error should be shown somewhere
+ in the UI. The expected error string is described in each individual test.
+ If you see the error, hit "Pay" again. If you don't see the error,
+ abort the payment request by hitting "esc" - which means that particular test
+ has failed.
+</p>
+<ol>
+ <li>
+ <button onclick="retryShowsPayerMember(this, { email: 'EMAIL ERROR' });">
+ The payment sheet shows "EMAIL ERROR" for the payer's email.
+ </button>
+ </li>
+ <li>
+ <button onclick="retryShowsPayerMember(this, { name: 'NAME ERROR' });">
+ The payment sheet shows "NAME ERROR" for the payer's name.
+ </button>
+ </li>
+ <li>
+ <button onclick="retryShowsPayerMember(this, { phone: 'PHONE ERROR' });">
+ The payment sheet shows "PHONE ERROR" for the payer's phone number.
+ </button>
+ </li>
+ <li>
+ <button onclick="done();">
+ Done!
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">owners</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/constructor_convert_method_data.https.html b/testing/web-platform/tests/payment-request/constructor_convert_method_data.https.html
new file mode 100644
index 0000000000..f4a9a721d0
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/constructor_convert_method_data.https.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html> <meta charset="utf-8" />
+<title>Validates PaymentMethodData's data member during construction</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>
+ const details = {
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "0.00",
+ },
+ },
+ };
+
+ test(() => {
+ new PaymentRequest([{ supportedMethods: "basic-card" }], details);
+ new PaymentRequest(
+ [{ supportedMethods: "https://apple.com/apple-pay" }],
+ details
+ );
+ }, "Smoke test.");
+
+ const knownPMIs = ["basic-card", "https://apple.com/apple-pay"];
+ const unknownPMIs = ["fake-pmi", "https://does-not.exist"];
+
+ promise_test(async t => {
+ for (const supportedMethods of [].concat(knownPMIs).concat(unknownPMIs)) {
+ const method = { supportedMethods };
+ const request = new PaymentRequest([method], details);
+ assert_throws_js(
+ TypeError,
+ () => {
+ const badMethod = Object.assign(
+ {},
+ method,
+ { data: 123 } // <- this will throw
+ );
+ new PaymentRequest([badMethod], details);
+ },
+ "PaymentMethodData.data can't be converted to an Object."
+ );
+ }
+ }, "Tries to convert data member during Payment Request construction, irrespective of PMI.");
+
+ promise_test(async t => {
+ for (const supportedMethods of knownPMIs) {
+ const method = { supportedMethods };
+ const request = new PaymentRequest([method], details);
+
+ // Only check the PMIs that are actually supported
+ if (!(await request.canMakePayment())) continue;
+
+ assert_throws_js(
+ TypeError,
+ () => {
+ const badMethod = Object.assign(
+ {},
+ method,
+ /* This is invalid in both Apple Pay and Basic Card */
+ { data: { supportedNetworks: "this will throw" } }
+ );
+ new PaymentRequest([badMethod], details);
+ },
+ "PaymentMethodData.data is invalid."
+ );
+ }
+ }, "Converts PaymentMethodData's data to mandated IDL type during PaymentRequest construction.");
+</script>
diff --git a/testing/web-platform/tests/payment-request/delegate-request.https.sub.html b/testing/web-platform/tests/payment-request/delegate-request.https.sub.html
new file mode 100644
index 0000000000..988550036c
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/delegate-request.https.sub.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<title>Payment request delegation test</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-actions.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+
+<div>
+ Verifies that PaymentRequest.show() call from a cross-origin subframe without user activation
+ works if and only if the top frame has user activation and it delegates the capability to the
+ subframe.
+</div>
+
+<iframe allow="payment" width="300px" height="50px"
+ src="https://{{hosts[alt][www]}}:{{ports[https][0]}}/payment-request/resources/delegate-request-subframe.sub.html">
+</iframe>
+
+<script>
+ // Returns a |Promise| that gets resolved with |event.data| when |window|
+ // receives from |source| a "message" event whose |event.data.type| matches the string
+ // |message_data_type|.
+ function getMessageData(message_data_type, source) {
+ return new Promise(resolve => {
+ function waitAndRemove(e) {
+ if (e.source != source || !e.data || e.data.type != message_data_type)
+ return;
+ window.removeEventListener("message", waitAndRemove);
+ resolve(e.data);
+ }
+ window.addEventListener("message", waitAndRemove);
+ });
+ }
+
+ promise_setup(async () => {
+ // Make sure the iframe has loaded.
+ await getMessageData("subframe-loaded", frames[0]);
+ });
+
+ const target_origin = "https://{{hosts[alt][www]}}:{{ports[https][0]}}";
+ const request = {"type": "make-payment-request"};
+
+ promise_test(async () => {
+ let result_promise = getMessageData("result", frames[0]);
+ frames[0].postMessage(request, {targetOrigin: target_origin});
+ let data = await result_promise;
+
+ assert_equals(data.result, "failure");
+ }, "Payment-request from a subframe fails without delegation when the top frame has no user activation");
+
+ promise_test(async () => {
+ let result_promise = getMessageData("result", frames[0]);
+ await test_driver.bless();
+ frames[0].postMessage(request, {targetOrigin: target_origin});
+ let data = await result_promise;
+
+ assert_equals(data.result, "failure");
+ }, "Payment-request from a subframe fails without delegation when the top frame has user activation");
+
+ promise_test(async () => {
+ let result_promise = getMessageData("result", frames[0]);
+ await test_driver.bless();
+ frames[0].postMessage(request, {targetOrigin: target_origin,
+ delegate: "payment"});
+ let data = await result_promise;
+
+ assert_equals(data.result, "success");
+ }, "Payment-request from a subframe succeeds with delegation when the top frame has user activation");
+
+ // This test must follow the successful test case above so that the user activation state there
+ // gets consumed.
+ promise_test(async () => {
+ let result_promise = getMessageData("result", frames[0]);
+ frames[0].postMessage(request, {targetOrigin: target_origin,
+ delegate: "payment"});
+ let data = await result_promise;
+
+ assert_equals(data.result, "failure");
+ }, "Payment-request from a subframe fails with delegation when the top frame has no user activation");
+
+</script>
diff --git a/testing/web-platform/tests/payment-request/historical.https.html b/testing/web-platform/tests/payment-request/historical.https.html
new file mode 100644
index 0000000000..aa183a58cd
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/historical.https.html
@@ -0,0 +1,45 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Historical Payment Request APIs</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+[
+ // https://github.com/w3c/browser-payment-api/pull/419
+ ["paymentRequestID", "PaymentRequest"],
+ ["paymentRequestID", "PaymentResponse"],
+
+ // https://github.com/w3c/browser-payment-api/pull/258
+ ["careOf", "PaymentAddress"],
+
+ // https://github.com/w3c/browser-payment-api/pull/219
+ ["totalAmount", "PaymentResponse"],
+
+ // https://github.com/w3c/browser-payment-api/pull/426
+ ["paymentRequestId", "PaymentRequest"],
+ ["paymentRequestId", "PaymentResponse"],
+
+ // https://github.com/w3c/payment-request/pull/765
+ ["languageCode", "PaymentAddress"],
+
+ //https://github.com/whatwg/html/pull/5915
+ ["allowPaymentRequest", "HTMLIFrameElement"],
+
+].forEach(([member, interf]) => {
+ test(() => {
+ assert_false(member in window[interf].prototype);
+ }, member + ' in ' + interf);
+});
+
+// https://github.com/w3c/payment-request/pull/551
+test(() => {
+ const methods = [];
+ const expectedError = {name: 'toString should be called'};
+ const unexpectedError = {name: 'sequence<DOMString> conversion is not allowed'};
+ methods.toString = () => { throw expectedError; };
+ Object.defineProperty(methods, '0', { get: () => { throw unexpectedError; } });
+ assert_throws_exactly(expectedError, () => {
+ new PaymentRequest([{supportedMethods: methods}], {total: {label: 'bar', amount: {currency: 'BAZ', value: '0'}}});
+ });
+}, 'supportedMethods must not support sequence<DOMString>');
+</script>
diff --git a/testing/web-platform/tests/payment-request/idlharness.https.window.js b/testing/web-platform/tests/payment-request/idlharness.https.window.js
new file mode 100644
index 0000000000..53ae47e892
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/idlharness.https.window.js
@@ -0,0 +1,31 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+'use strict';
+
+// https://w3c.github.io/payment-request/
+
+idl_test(
+ ['payment-request'],
+ ['dom', 'html'],
+ idlArray => {
+ try {
+ const methods = [
+ {supportedMethods: 'basic-card'},
+ {supportedMethods: 'https://apple.com/apple-pay'},
+ ];
+ const amount = {currency: 'USD', value: '0'};
+ const details = {total: {label: 'label', amount: amount} };
+ window.paymentRequest = new PaymentRequest(methods, details);
+ } catch (e) {
+ // Surfaced below when paymentRequest is undefined.
+ }
+
+ idlArray.add_objects({
+ PaymentRequest: ['paymentRequest'],
+ PaymentMethodChangeEvent: ['new PaymentMethodChangeEvent("paymentmethodchange")'],
+ PaymentRequestUpdateEvent: ['new PaymentRequestUpdateEvent("paymentrequestupdate")'],
+ MerchantValidationEvent: ['new MerchantValidationEvent("merchantvalidation")'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/payment-request/onpaymentmethodchange-attribute.https.html b/testing/web-platform/tests/payment-request/onpaymentmethodchange-attribute.https.html
new file mode 100644
index 0000000000..f641bec4aa
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/onpaymentmethodchange-attribute.https.html
@@ -0,0 +1,79 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for onpaymentmethodchange attribute</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#onpaymentmethodchange-attribute">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+const testMethod = Object.freeze({ supportedMethods: "not-a-real-method" });
+const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ }
+});
+const defaultMethods = Object.freeze([testMethod, applePay]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+test(() => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ assert_idl_attribute(request, "onpaymentmethodchange");
+}, "Must have a onpaymentmethodchange IDL attribute");
+
+test(() => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const ev = new Event("paymentmethodchange");
+ let didHandle = false;
+ request.onpaymentmethodchange = evt => {
+ assert_equals(ev, evt, "must be same event");
+ didHandle = true;
+ };
+ request.dispatchEvent(ev);
+ assert_true(didHandle, "event did not fire");
+}, `onpaymentmethodchange attribute is a generic handler for "paymentmethodchange"`);
+
+test(() => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const ev = new PaymentMethodChangeEvent("paymentmethodchange");
+ let didHandle = false;
+ request.onpaymentmethodchange = evt => {
+ assert_equals(ev, evt, "must be same event");
+ didHandle = true;
+ };
+ request.dispatchEvent(ev);
+ assert_true(didHandle, "event did not fire");
+}, `onpaymentmethodchange attribute is a handler for PaymentMethodChangeEvent`);
+
+test(() => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const ev = new PaymentMethodChangeEvent("paymentmethodchange", {
+ methodName: "test"
+ });
+ let didHandle = false;
+ let didListen = false;
+ request.onpaymentmethodchange = evt => {
+ assert_equals(ev, evt, "must be same event");
+ didHandle = true;
+ };
+ request.addEventListener("paymentmethodchange", evt => {
+ assert_equals(ev, evt, "must be same event");
+ didListen = true;
+ });
+ request.dispatchEvent(ev);
+ assert_true(didHandle, "onpaymentmethodchange did not receive the event");
+ assert_true(didListen, "addEventListener did not receive the event");
+}, `onpaymentmethodchange attribute and listeners both work`);
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-is-showing.https.html b/testing/web-platform/tests/payment-request/payment-is-showing.https.html
new file mode 100644
index 0000000000..a30029458f
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-is-showing.https.html
@@ -0,0 +1,170 @@
+<!DOCTYPE html> <meta charset="utf-8" />
+<title>Test for PaymentRequest.show(optional promise) method</title>
+<link
+ rel="help"
+ href="https://w3c.github.io/browser-payment-api/#dfn-payment-request-is-showing"
+/>
+<meta name="timeout" content="long" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<body>
+ <script>
+ "use strict";
+ const applePayMethod = {
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+ };
+ const methods = [{ supportedMethods: "basic-card" }, applePayMethod];
+ const details = {
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+ };
+
+ /**
+ * Attaches an iframe to window.document.
+ *
+ * @param {String} src Optional resource URL to load.
+ * @returns {Promise} Resolves when the src loads.
+ */
+ async function attachIframe(src = "./resources/blank.html") {
+ const iframe = document.createElement("iframe");
+ iframe.allow = "payment";
+ iframe.src = src;
+ document.body.appendChild(iframe);
+ await new Promise((resolve) => {
+ iframe.addEventListener("load", resolve, { once: true });
+ });
+ return iframe;
+ }
+
+ function getShowPromiseFromContext(paymentRequest, context = this) {
+ return test_driver.bless(
+ "payment request show()",
+ () => {
+ return [paymentRequest.show()];
+ },
+ context
+ );
+ }
+
+ promise_test(async (t) => {
+ const request1 = new PaymentRequest(methods, details);
+ const request2 = new PaymentRequest(methods, details);
+
+ // Sets the "payment-relevant browsing context's payment request is
+ // showing boolean" to true and then try to show a second payment sheet in
+ // the same window. The second show() should reject.
+ await test_driver.bless("payment request show()");
+ const showPromise1 = request1.show();
+
+ await test_driver.bless("payment request show()");
+ const showPromise2 = request2.show();
+
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ showPromise2,
+ "Attempting to show a second payment request must reject."
+ );
+
+ await request1.abort();
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ showPromise1,
+ "request1 was aborted via .abort()"
+ );
+
+ // Finally, request2 should have been "closed", so trying to show
+ // it will again result in promise rejected with an InvalidStateError.
+ // See: https://github.com/w3c/payment-request/pull/821
+ const rejectedPromise = request2.show();
+ await promise_rejects_dom(
+ t,
+ "InvalidStateError",
+ rejectedPromise,
+ "Attempting to show a second payment request must reject."
+ );
+ // Finally, we confirm that request2's returned promises are unique.
+ assert_not_equals(
+ showPromise2,
+ rejectedPromise,
+ "Returned Promises be unique"
+ );
+ }, "The top browsing context can only show one payment sheet at a time.");
+
+ promise_test(async (t) => {
+ const iframe = await attachIframe();
+ const iframeWindow = iframe.contentWindow;
+
+ // Payment requests
+ const windowRequest = new window.PaymentRequest(methods, details);
+ const iframeRequest = new iframeWindow.PaymentRequest(methods, details);
+
+ // Let's get some blessed showPromises
+ // iframe sets "is showing boolean", ignore the returned promise.
+ const [iframePromise] = await getShowPromiseFromContext(
+ iframeRequest,
+ iframeWindow
+ );
+
+ // The top level window now tries to show() the payment request.
+ await test_driver.bless("payment request show()");
+ const showPromise = windowRequest.show();
+
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ showPromise,
+ "iframe is already showing a payment request."
+ );
+
+ // Cleanup
+ await iframeRequest.abort();
+ iframe.remove();
+ }, "If an iframe shows a payment request, the top-level browsing context can't also show one.");
+
+ promise_test(async (t) => {
+ const iframe = await attachIframe();
+ const iframeWindow = iframe.contentWindow;
+ const iframeRequest = new iframeWindow.PaymentRequest(methods, details);
+ const [iframeShowPromise] = await getShowPromiseFromContext(
+ iframeRequest,
+ iframeWindow
+ );
+
+ // We navigate away, causing the payment sheet to close
+ // and the request is showing boolean to become false.
+ await new Promise((resolve) => {
+ iframe.onload = resolve;
+ iframe.src = "./resources/blank.html?test=123";
+ });
+
+ iframe.remove();
+
+ // Now we should be ok to spin up a new payment request
+ const request = new window.PaymentRequest(methods, details);
+ const [showPromise] = await getShowPromiseFromContext(request);
+ await request.abort();
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ showPromise,
+ "Normal abort."
+ );
+ }, "Navigating an iframe as a nested browsing context sets 'payment request is showing boolean' to false.");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/payment-request/payment-request-abort-method.https.html b/testing/web-platform/tests/payment-request/payment-request-abort-method.https.html
new file mode 100644
index 0000000000..32b87970b4
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-abort-method.https.html
@@ -0,0 +1,91 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Test for PaymentRequest.abort() method</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#abort-method" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+"use strict";
+setup({
+ // Ignore unhandled rejections resulting from .show()'s acceptPromise
+ // not being explicitly handled.
+ allow_uncaught_exception: true,
+ explicit_timeout: true,
+});
+const basicCard = Object.freeze({ supportedMethods: "basic-card" });
+const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+});
+const defaultMethods = Object.freeze([basicCard, applePay]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+promise_test(async t => {
+ // request is in "created" state
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ await promise_rejects_dom(t, "InvalidStateError", request.abort());
+}, `Throws if the promise [[state]] is not "interactive"`);
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const promises = new Set([
+ request.abort(),
+ request.abort(),
+ request.abort(),
+ ]);
+ assert_equals(promises.size, 3, "Must have three unique objects");
+}, "Calling abort() multiple times is always a new object.");
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ await test_driver.bless("show payment request");
+ const acceptPromise = request.show();
+ await request.abort();
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+ // As request is now "closed", trying to show it will fail
+ await test_driver.bless("show payment request");
+ await promise_rejects_dom(t, "InvalidStateError", request.show());
+}, "The same request cannot be shown multiple times.");
+
+promise_test(async t => {
+ // request is in "created" state.
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ await promise_rejects_dom(t, "InvalidStateError", request.abort());
+ // Call it again, for good measure.
+ await promise_rejects_dom(t, "InvalidStateError", request.abort());
+ // The request's state is "created", so let's show it
+ // which changes the state to "interactive.".
+ await test_driver.bless("show payment request");
+ const acceptPromise = request.show();
+ // Let's set request the state to "closed" by calling .abort()
+ await request.abort();
+ // The request is now "closed", so...
+ await promise_rejects_dom(t, "InvalidStateError", request.abort());
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+}, "Aborting a request before it is shown doesn't prevent it from being shown later.");
+</script>
+<small>
+ If you find a buggy test, please
+ <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a> and
+ tag one of the
+ <a
+ href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml"
+ >suggested reviewers</a
+ >.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-request-canmakepayment-method.https.html b/testing/web-platform/tests/payment-request/payment-request-canmakepayment-method.https.html
new file mode 100644
index 0000000000..f02474de12
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-canmakepayment-method.https.html
@@ -0,0 +1,120 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for PaymentRequest.canMakePayment() method</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#canmakepayment-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script>
+"use strict";
+const basicCard = Object.freeze({ supportedMethods: "basic-card" });
+const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ }
+});
+const defaultMethods = Object.freeze([basicCard, applePay]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+const unsupportedMethods = [
+ { supportedMethods: "this-is-not-supported" },
+ { supportedMethods: "https://not.supported" },
+];
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ assert_true(
+ await request.canMakePayment(),
+ "one of the methods should be supported"
+ );
+}, `If payment method identifier are supported, resolve promise with true.`);
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ try {
+ assert_true(
+ await request.canMakePayment(),
+ `canMakePaymentPromise should be true`
+ );
+ assert_true(
+ await request.canMakePayment(),
+ `canMakePaymentPromise should be true`
+ );
+ } catch (err) {
+ assert_equals(
+ err.name,
+ "NotAllowedError",
+ "if it throws, then it must be a NotAllowedError."
+ );
+ }
+}, `If request.[[state]] is "created", then return a promise that resolves to true for known method.`);
+
+promise_test(async t => {
+ const noneSupported = new PaymentRequest(
+ unsupportedMethods,
+ defaultDetails
+ ).canMakePayment();
+ assert_false(await noneSupported, `methods must not be supported`);
+}, "All methods are unsupported");
+
+promise_test(async t => {
+ const someSupported = new PaymentRequest(
+ [...unsupportedMethods, ...defaultMethods],
+ defaultDetails
+ ).canMakePayment();
+ assert_true(await someSupported, `At least one method is expected to be supported.`);
+}, `Mix of supported and unsupported methods, at least one method is supported.`);
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const [acceptPromise, canMakePaymentPromise] = await test_driver.bless(
+ "show payment request",
+ () => {
+ const acceptPromise = request.show(); // Sets state to "interactive"
+ const canMakePaymentPromise = request.canMakePayment();
+ return [acceptPromise, canMakePaymentPromise];
+ });
+
+ await promise_rejects_dom(t, "InvalidStateError", canMakePaymentPromise);
+ request.abort();
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+
+ // The state should be "closed"
+ await promise_rejects_dom(t, "InvalidStateError", request.canMakePayment());
+}, 'If request.[[state]] is "interactive", then return a promise rejected with an "InvalidStateError" DOMException.');
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const [abortPromise, acceptPromise] = await test_driver.bless(
+ "show payment request",
+ () => {
+ const acceptPromise = request.show(); // Sets state to "interactive"
+ acceptPromise.catch(() => {}); // no-op, just to silence unhandled rejection in devtools.
+ const abortPromise = request.abort(); // Sets state to "closed"
+ return [abortPromise, acceptPromise];
+ });
+
+ await abortPromise;
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+ await promise_rejects_dom(t, "InvalidStateError", request.canMakePayment());
+}, 'If request.[[state]] is "closed", then return a promise rejected with an "InvalidStateError" DOMException.');
+</script>
+
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-request-constructor.https.sub.html b/testing/web-platform/tests/payment-request/payment-request-constructor.https.sub.html
new file mode 100644
index 0000000000..c1ecc22583
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-constructor.https.sub.html
@@ -0,0 +1,477 @@
+<!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>
diff --git a/testing/web-platform/tests/payment-request/payment-request-ctor-currency-code-checks.https.sub.html b/testing/web-platform/tests/payment-request/payment-request-ctor-currency-code-checks.https.sub.html
new file mode 100644
index 0000000000..c608608c7e
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-ctor-currency-code-checks.https.sub.html
@@ -0,0 +1,282 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test currency code usage in 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>
+const defaultMethods = [
+ Object.freeze({
+ supportedMethods: "https://{{domains[nonexistent]}}",
+ }),
+];
+const defaultAmount = Object.freeze({
+ currency: "USD",
+ value: "1.00",
+});
+const defaultTotal = Object.freeze({
+ label: "Total",
+ amount: defaultAmount,
+});
+
+const defaultDetails = Object.freeze({
+ total: Object.freeze({
+ label: "",
+ amount: defaultAmount,
+ }),
+});
+
+// The following are the same set of valid/invalid codes that are used in
+// the ECMAScript Internationalization API Specification (ECMA-402) test suite.
+const wellFormedCurrencyCodes = [
+ "BOB",
+ "EUR",
+ "usd", // currency codes are case-insensitive
+ "XdR",
+ "xTs",
+];
+
+const invalidCurrencyCodes = [
+ "",
+ "€",
+ "$",
+ "SFr.",
+ "DM",
+ "KR₩",
+ "702",
+ "ßP",
+ "ınr",
+];
+
+const RANGE_ERROR = RangeError;
+
+const invalidAmount = Object.freeze({
+ currency: "¡INVALID!",
+ value: "1.00",
+});
+
+const invalidTotal = {
+ total: {
+ label: "Invalid total",
+ amount: invalidAmount,
+ },
+};
+
+// Ensure we don't get false positives
+function smokeTest() {
+ new PaymentRequest(defaultMethods, invalidTotal);
+}
+
+// Process the total:
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const validCurrency of wellFormedCurrencyCodes) {
+ const amount = {
+ currency: validCurrency,
+ value: "1.00",
+ };
+ const total = {
+ label: "Total",
+ amount,
+ };
+ const details = {
+ total,
+ };
+ try {
+ new PaymentRequest(defaultMethods, details);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected exception for details.total.amount "${validCurrency}": ${err.message}`
+ );
+ }
+ }
+}, "Check and canonicalize valid details.total.amount");
+
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const invalidCurrency of invalidCurrencyCodes) {
+ const amount = {
+ currency: invalidCurrency,
+ value: "1.00",
+ };
+ const total = {
+ label: "Total",
+ amount,
+ };
+ const details = {
+ total,
+ };
+ assert_throws_js(
+ RANGE_ERROR,
+ () => {
+ new PaymentRequest(defaultMethods, details);
+ },
+ `Expected RangeError for details.total.amount given ("${invalidCurrency}").`
+ );
+ }
+}, "Check and canonicalize invalid details.total.amount and rethrow any exceptions.");
+
+// If the displayItems member of details is present, then for each item in details.displayItems:
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ const displayItems = [];
+ for (const validCurrency of wellFormedCurrencyCodes) {
+ const amount = {
+ currency: validCurrency,
+ value: "123",
+ };
+ const displayItem = {
+ amount,
+ label: "valid currency",
+ };
+ const details = {
+ total: defaultTotal,
+ displayItems: [displayItem],
+ };
+ try {
+ new PaymentRequest(defaultMethods, details);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected error with displayItem that had a valid currency "${validCurrency}": ${err.message}`
+ );
+ }
+ displayItems.push(displayItem);
+ }
+ // Let's make sure it doesn't throw given a list of valid displayItems
+ try {
+ const details = Object.assign({}, defaultDetails, { displayItems });
+ new PaymentRequest(defaultMethods, details);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected error with multiple valid displayItems: ${err.message}`
+ );
+ }
+}, "Check and canonicalize valid details.displayItems amount");
+
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const invalidCurrency of invalidCurrencyCodes) {
+ const amount = {
+ currency: invalidCurrency,
+ value: "123",
+ };
+ const displayItem = {
+ amount,
+ label: "invalid currency",
+ };
+ const details = {
+ total: defaultTotal,
+ displayItems: [displayItem],
+ };
+ assert_throws_js(
+ RANGE_ERROR,
+ () => {
+ new PaymentRequest(defaultMethods, details);
+ },
+ `Expected RangeError with invalid displayItem currency "${invalidCurrency}".`
+ );
+ }
+}, "Check and canonicalize invalid details.displayItems amount and rethrow RangeError.");
+
+// Process payment details modifiers:
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const validCurrency of wellFormedCurrencyCodes) {
+ const modifier = {
+ supportedMethods: "https://{{domains[nonexistent]}}",
+ total: {
+ label: "Total due",
+ amount: { currency: validCurrency, value: "68.00" },
+ },
+ };
+ const details = {
+ total: defaultTotal,
+ modifiers: [modifier],
+ };
+ try {
+ new PaymentRequest(defaultMethods, details);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected error with valid modifier currency code "${validCurrency}": ${err.message}`
+ );
+ }
+ }
+}, "Check and canonicalize valid modifiers[n].total amount.");
+
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const invalidCurrency of invalidCurrencyCodes) {
+ const modifier = {
+ supportedMethods: "https://{{domains[nonexistent]}}",
+ total: {
+ label: "Total due",
+ amount: { currency: invalidCurrency, value: "68.00" },
+ },
+ };
+ const details = {
+ total: defaultTotal,
+ modifiers: [modifier],
+ };
+ assert_throws_js(
+ RANGE_ERROR,
+ () => {
+ new PaymentRequest(defaultMethods, details);
+ },
+ `Expected RangeError with invalid modifier currency code "${invalidCurrency}".`
+ );
+ }
+}, "Check and canonicalize invalid modifiers[n].total amount and rethrow RangeError.");
+
+// Process payment details modifiers:
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const validCurrency of wellFormedCurrencyCodes) {
+ const additionalItem = {
+ label: "additionalItem",
+ amount: { currency: validCurrency, value: "3.00" },
+ };
+ const modifier = {
+ supportedMethods: "https://{{domains[nonexistent]}}",
+ total: defaultTotal,
+ additionalDisplayItems: [additionalItem],
+ };
+ const details = {
+ total: defaultTotal,
+ modifiers: [modifier],
+ };
+ try {
+ new PaymentRequest(defaultMethods, details);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected error with valid additionalDisplayItems[n] currency code "${validCurrency}": ${err.message}`
+ );
+ }
+ }
+}, "Check and canonicalize valid modifiers[n].additionaDisplayItem amount.");
+
+test(() => {
+ assert_throws_js(RANGE_ERROR, smokeTest, "Expected smoke test to throw.");
+ for (const invalidCurrency of invalidCurrencyCodes) {
+ const additionalItem = {
+ label: "additionalItem",
+ amount: { currency: invalidCurrency, value: "3.00" },
+ };
+ const modifier = {
+ supportedMethods: "https://{{domains[nonexistent]}}",
+ total: defaultTotal,
+ additionalDisplayItems: [additionalItem],
+ };
+ const details = {
+ total: defaultTotal,
+ modifiers: [modifier],
+ };
+ assert_throws_js(
+ RANGE_ERROR,
+ () => {
+ new PaymentRequest(defaultMethods, details);
+ },
+ `Expected RangeError with invalid additionalDisplayItems[n] currency code "${invalidCurrency}".`
+ );
+ }
+}, "Check and canonicalize invalid modifiers[n].additionaDisplayItem amount and rethrow RangeError.");
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-request-ctor-pmi-handling.https.sub.html b/testing/web-platform/tests/payment-request/payment-request-ctor-pmi-handling.https.sub.html
new file mode 100644
index 0000000000..d6a1be2394
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-ctor-pmi-handling.https.sub.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<!-- Copyright © 2017 Mozilla and World Wide Web Consortium, (Massachusetts Institute of Technology, ERCIM, Keio University, Beihang). -->
+<meta charset="utf-8">
+<title>Test for validity of payment method identifiers during construction</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 validAmount = Object.freeze({
+ currency: "USD",
+ value: "1.0",
+});
+const validTotal = Object.freeze({
+ label: "Default Total",
+ amount: validAmount,
+});
+const defaultDetails = Object.freeze({
+ total: validTotal,
+});
+
+test(() => {
+ const validMethods = [
+ "https://wpt",
+ "https://{{domains[nonexistent]}}/",
+ "https://{{domains[nonexistent]}}/payment",
+ "https://{{domains[nonexistent]}}/payment-request",
+ "https://{{domains[nonexistent]}}/payment-request?",
+ "https://{{domains[nonexistent]}}/payment-request?this=is",
+ "https://{{domains[nonexistent]}}/payment-request?this=is&totally",
+ "https://{{domains[nonexistent]}}:443/payment-request?this=is&totally",
+ "https://{{domains[nonexistent]}}:443/payment-request?this=is&totally#fine",
+ "https://:@{{domains[nonexistent]}}:443/payment-request?this=is&totally#👍",
+ " \thttps://wpt\n ",
+ "https://xn--c1yn36f",
+ "https://點看",
+ ];
+ for (const validMethod of validMethods) {
+ try {
+ const methods = [{ supportedMethods: validMethod }];
+ new PaymentRequest(methods, defaultDetails);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected exception with valid standardized PMI: ${validMethod}. ${err}`
+ );
+ }
+ }
+}, "Must support valid standard URL PMIs");
+
+test(() => {
+ const validMethods = [
+ "e",
+ "n6jzof05mk2g4lhxr-u-q-w1-c-i-pa-ty-bdvs9-ho-ae7-p-md8-s-wq3-h-qd-e-q-sa",
+ "a-b-q-n-s-pw0",
+ "m-u",
+ "s-l5",
+ "k9-f",
+ "m-l",
+ "u4-n-t",
+ "i488jh6-g18-fck-yb-v7-i",
+ "x-x-t-t-c34-o",
+ "secure-payment-confirmation",
+ // gets coerced to "secure-payment-confirmation", for compat with old version of spec
+ ["secure-payment-confirmation"],
+ ];
+ for (const validMethod of validMethods) {
+ try {
+ const methods = [{ supportedMethods: validMethod }];
+ new PaymentRequest(methods, defaultDetails);
+ } catch (err) {
+ assert_unreached(
+ `Unexpected exception with valid standardized PMI: ${validMethod}. ${err}`
+ );
+ }
+ }
+}, "Must not throw on syntactically valid standardized payment method identifiers, even if they are not supported");
+
+test(() => {
+ const invalidMethods = [
+ "secure-💳",
+ "¡secure-*-payment-confirmation!",
+ "Secure-Payment-Confirmation",
+ "0",
+ "-",
+ "--",
+ "a--b",
+ "-a--b",
+ "a-b-",
+ "0-",
+ "0-a",
+ "a0--",
+ "A-",
+ "A-B",
+ "A-b",
+ "a-0",
+ "a-0b",
+ " a-b",
+ "\t\na-b",
+ "a-b ",
+ "a-b\n\t",
+ "secure-payment-confirmation?not-really",
+ "secure-payment-confirmation://not-ok",
+ "secure payment confirmation",
+ "/secure payment confirmation/",
+ "SeCuRePaYmEnTcOnFiRmAtIoN",
+ "SECURE-PAYMENT-CONFIRMATION",
+ " secure-payment-confirmation ",
+ "this is not supported",
+ " ",
+ "foo,var",
+ ["visa","mastercard"], // stringifies to "visa,mastercard"
+ ];
+ for (const invalidMethod of invalidMethods) {
+ assert_throws_js(
+ RangeError,
+ () => {
+ const methods = [{ supportedMethods: invalidMethod }];
+ new PaymentRequest(methods, defaultDetails);
+ },
+ `expected RangeError processing invalid standardized PMI "${invalidMethod}"`
+ );
+ }
+}, "Must throw on syntactically invalid standardized payment method identifiers");
+
+test(() => {
+ const invalidMethods = [
+ "https://username@example.com/pay",
+ "https://:password@example.com/pay",
+ "https://username:password@example.com/pay",
+ "http://username:password@example.com/pay",
+ "http://foo.com:100000000/pay",
+ "not-https://{{domains[nonexistent]}}/payment-request",
+ "../realitive/url",
+ "/absolute/../path?",
+ "https://",
+ ];
+ for (const invalidMethod of invalidMethods) {
+ assert_throws_js(
+ RangeError,
+ () => {
+ const methods = [{ supportedMethods: invalidMethod }];
+ new PaymentRequest(methods, defaultDetails);
+ },
+ `expected RangeError processing invalid URL PMI "${invalidMethod}"`
+ );
+ }
+}, "Constructor MUST throw if given an invalid URL-based payment method identifier");
+
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-request-disallowed-when-hidden.https.html b/testing/web-platform/tests/payment-request/payment-request-disallowed-when-hidden.https.html
new file mode 100644
index 0000000000..3a5eb01015
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-disallowed-when-hidden.https.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for PaymentRequest.show() method - should fail when tab is not visible</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#show-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<!-- For minimize() -->
+<script src="/page-visibility/resources/window_state_context.js"></script>
+<script>
+'use strict';
+
+promise_test(async t => {
+ const {minimize, restore} = window_state_context(t);
+
+ const request = new PaymentRequest([
+ {
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+ },
+ ], {
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ }
+ });
+
+ // `bless` simulates a click so it must happen before minimizing the window.
+ await test_driver.bless('user activation');
+
+ // Before we trigger the Payment Request, minimize the window. This should
+ // cause the show() call to be rejected.
+ await minimize();
+ assert_equals(document.hidden, true);
+
+ return promise_rejects_dom(t, "AbortError", request.show());
+}, 'PaymentRequest.show() cannot be triggered from a hidden context');
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-manual.tentative.https.html b/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-manual.tentative.https.html
new file mode 100644
index 0000000000..e6b164f7cc
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-manual.tentative.https.html
@@ -0,0 +1,95 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Manual tests for PaymentRequest.hasEnrolledInstrument() method</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#hasenrolledinstrument-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({
+ explicit_done: true,
+ explicit_timeout: true,
+});
+
+const defaultMethods = Object.freeze([
+ {
+ supportedMethods: "basic-card",
+ data: {
+ supportedNetworks: [ 'visa' ],
+ },
+ }
+]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+function testHasNoEnrolledInstrument() {
+ promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ assert_false(
+ await request.hasEnrolledInstrument(),
+ "No test enrolled in the test profile."
+ );
+ }, `hasEnrolledInstrument() resolves to false when user has no enrolled instrument.`);
+}
+
+function testHasEnrolledInstrument() {
+ promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ assert_true(
+ await request.hasEnrolledInstrument(),
+ "A card is enrolled in the test profile."
+ );
+ }, `hasEnrolledInstrument() resolves to true when user has an enrolled instrument.`);
+}
+
+function testHasEnrolledInstrumentAgain() {
+ promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ assert_true(
+ await request.hasEnrolledInstrument(),
+ "A card is enrolled in the test profile."
+ );
+ }, `hasEnrolledInstrument() can be called multiple times if the payment method details are identical.`);
+}
+</script>
+
+<h2>Manual tests for hasEnrolledInstrument() method</h2>
+<p>
+ Follow the instructions from top to bottom. Click on each button in sequence
+ without refreshing the page. Some of the tests will bring up the Payment
+ Request UI and close them automatically. If a payment sheet stays open, the
+ test has failed.
+</p>
+<ol>
+ <li>Follow browser-specific instructions to remove all cards from the test profile.</li>
+ <li>
+ <button onclick="testHasNoEnrolledInstrument()">
+ hasEnrolledInstrument() resolves to false when user has no enrolled instrument.
+ </button>
+ </li>
+ <li>Add a test Visa card to your test profile, e.g. 4012888888881881.</li>
+ <li>
+ <button onclick="testHasEnrolledInstrument()">
+ hasEnrolledInstrument() resolves to true when user has an enrolled instrument.
+ </button>
+ </li>
+ <li>
+ <button onclick="testHasEnrolledInstrumentAgain()">
+ hasEnrolledInstrument() can be called multiple times if the payment method
+ details are identical.
+ </button>
+ </li>
+ <li>
+ <button onclick="done()">Done!</button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-protection.tentative.https.html b/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-protection.tentative.https.html
new file mode 100644
index 0000000000..4da11304a2
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method-protection.tentative.https.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for PaymentRequest.hasEnrolledInstrument() method</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#hasenrolledinstrument-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script>
+const visaMethod = Object.freeze({
+ supportedMethods: "basic-card",
+ data: {
+ supportedNetworks: ['visa']
+ }
+});
+const mastercardMethod = Object.freeze({
+ supportedMethods: "basic-card",
+ data: {
+ supportedNetworks: ['mastercard']
+ }
+});
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+promise_test(async t => {
+ // This test may never actually hit its assertion, but that's allowed.
+ const request = new PaymentRequest([visaMethod], defaultDetails);
+ for (let i = 0; i < 1000; i++) {
+ try {
+ await request.hasEnrolledInstrument();
+ } catch (err) {
+ assert_equals(
+ err.name,
+ "NotAllowedError",
+ "If it throws, then it must be a NotAllowedError."
+ );
+ break;
+ }
+ }
+
+ for (let i = 0; i < 1000; i++) {
+ try {
+ const request2 = new PaymentRequest([mastercardMethod], defaultDetails);
+ await request2.hasEnrolledInstrument();
+ } catch (err) {
+ assert_equals(
+ err.name,
+ "NotAllowedError",
+ "If it throws, then it must be a NotAllowedError."
+ );
+ break;
+ }
+ }
+}, `Optionally, at the user agent's discretion, return a promise rejected with a "NotAllowedError" DOMException.`);
+
+</script>
+
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method.tentative.https.html b/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method.tentative.https.html
new file mode 100644
index 0000000000..4f6b7e9239
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-hasenrolledinstrument-method.tentative.https.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Tests for PaymentRequest.hasEnrolledInstrument() method</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#hasenrolledinstrument-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src='/resources/testdriver-vendor.js'></script>
+<script>
+"use strict";
+
+setup({ allow_uncaught_exception: true });
+
+const unsupportedMethods = [
+ { supportedMethods: "this-is-not-supported" },
+ { supportedMethods: "https://not.supported" },
+];
+const defaultMethods = Object.freeze([
+ {
+ supportedMethods: "basic-card",
+ data: {
+ supportedNetworks: [ 'visa' ],
+ },
+ }
+]);
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+promise_test(async t => {
+ const request = new PaymentRequest(unsupportedMethods, defaultDetails);
+ assert_false(
+ await request.hasEnrolledInstrument(),
+ "hasEnrolledInstrument() should resolve to false."
+ );
+}, `hasEnrolledInstrument() resolves to false for unsupported payment methods.`);
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const [acceptPromise, hasEnrolledInstrumentPromise] = await test_driver.bless(
+ "show payment request",
+ () => {
+ const acceptPromise = request.show(); // Sets state to "interactive"
+ const hasEnrolledInstrumentPromise = request.hasEnrolledInstrument();
+ return [acceptPromise, hasEnrolledInstrumentPromise];
+ });
+ await promise_rejects_dom(t, "InvalidStateError", hasEnrolledInstrumentPromise);
+
+ await request.abort();
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+}, `If request.[[state]] is "interactive", then return a promise rejected with an "InvalidStateError" DOMException.`);
+
+promise_test(async t => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ const [abortPromise, acceptPromise] = await test_driver.bless( "show payment request", () => {
+ const acceptPromise = request.show(); // Sets state to "interactive"
+ acceptPromise.catch(() => {}); // no-op, just to handle unhandled rejection in devtools.
+ const abortPromise =request.abort(); // Sets state to "closed"
+ return [abortPromise, acceptPromise];
+ });
+ await abortPromise;
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+
+ const hasEnrolledInstrumentPromise = request.hasEnrolledInstrument();
+ await promise_rejects_dom(t, "InvalidStateError", hasEnrolledInstrumentPromise);
+}, `If request.[[state]] is "closed", then return a promise rejected with an "InvalidStateError" DOMException.`);
+</script>
+
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-request-id-attribute.https.html b/testing/web-platform/tests/payment-request/payment-request-id-attribute.https.html
new file mode 100644
index 0000000000..e5d0c7a66e
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-id-attribute.https.html
@@ -0,0 +1,39 @@
+<!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 id attribute</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#constructor">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const methods = [{ supportedMethods: "foo" }];
+const total = { label: "label", amount: { currency: "USD", value: "5.00" } };
+
+test(() => {
+ const request1 = new PaymentRequest(methods, {
+ id: "pass",
+ total,
+ });
+ assert_idl_attribute(request1, "id");
+ assert_equals(request1.id, "pass", "Expected PaymentRequest.id to be 'pass'");
+}, "PaymentRequest's id attribute's value can be set via PaymentDetailsInit dictionary");
+
+// Test for https://github.com/w3c/payment-request/pull/665
+test(() => {
+ const uuidRegExp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ const request1 = new PaymentRequest(methods, {
+ total,
+ });
+ const request2 = new PaymentRequest(methods, {
+ total,
+ });
+ assert_true(
+ uuidRegExp.test(request1.id) && uuidRegExp.test(request2.id) ,
+ "Expected PaymentRequest.id be a UUID"
+ );
+ assert_not_equals(
+ request1.id, request2.id,
+ "Expected PaymentRequest.id be unique per instance"
+ );
+}, "PaymentRequest's id attribute must be a UUID when PaymentDetailsInit.id is missing");
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-request-insecure.http.html b/testing/web-platform/tests/payment-request/payment-request-insecure.http.html
new file mode 100644
index 0000000000..02122203d5
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-insecure.http.html
@@ -0,0 +1,13 @@
+<!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 (insecure)</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#paymentrequest-interface">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ assert_false(isSecureContext);
+ assert_false("PaymentRequest" in window);
+}, "PaymentRequest constructor must not be exposed in insecure context");
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-request-not-exposed.https.worker.js b/testing/web-platform/tests/payment-request/payment-request-not-exposed.https.worker.js
new file mode 100644
index 0000000000..e5576e6735
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-not-exposed.https.worker.js
@@ -0,0 +1,7 @@
+importScripts("/resources/testharness.js");
+
+test(() => {
+ assert_true(isSecureContext);
+ assert_false('PaymentRequest' in self);
+}, "PaymentRequest constructor must not be exposed in worker global scope");
+done();
diff --git a/testing/web-platform/tests/payment-request/payment-request-show-method.https.html b/testing/web-platform/tests/payment-request/payment-request-show-method.https.html
new file mode 100644
index 0000000000..d3385b5468
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-request-show-method.https.html
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Test for PaymentRequest.show() method</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#show-method" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+"use strict";
+
+setup({ allow_uncaught_exception: true });
+
+const defaultMethods = Object.freeze([
+ { supportedMethods: "basic-card" },
+ {
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+ },
+]);
+
+const defaultDetails = Object.freeze({
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+promise_test(async (t) => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ await promise_rejects_dom(t, "SecurityError", request.show());
+
+ await test_driver.bless();
+ // Sets state to "interactive", but consumes user interaction.
+ const acceptPromise = request.show();
+ await promise_rejects_dom(t, "SecurityError", request.show());
+
+ // With user activation, but already showing the sheet...
+ await test_driver.bless();
+ await promise_rejects_dom(t, "InvalidStateError", request.show());
+
+ await request.abort();
+ await promise_rejects_dom(t, "AbortError", acceptPromise);
+}, "Throws if the promise [[state]] is not 'created'.");
+
+promise_test(async (t) => {
+ const request1 = new PaymentRequest(defaultMethods, defaultDetails);
+ await test_driver.bless();
+ const acceptPromise1 = request1.show();
+
+ // Payment request already showing, so...
+ const request2 = new PaymentRequest(defaultMethods, defaultDetails);
+ await test_driver.bless();
+ await promise_rejects_dom(t, "AbortError", request2.show());
+
+ await request1.abort();
+ await promise_rejects_dom(t, "AbortError", acceptPromise1);
+}, `If the user agent's "payment request is showing" boolean is true, then return a promise rejected with an "AbortError" DOMException.`);
+
+promise_test(async (t) => {
+ const request = new PaymentRequest(
+ [{ supportedMethods: "this-is-not-supported" }],
+ defaultDetails
+ );
+ await test_driver.bless();
+ const acceptPromise = request.show();
+ await promise_rejects_dom(t, "NotSupportedError", acceptPromise);
+}, `If payment method consultation produces no supported method of payment, then return a promise rejected with a "NotSupportedError" DOMException.`);
+
+promise_test(async (t) => {
+ const request = new PaymentRequest(defaultMethods, defaultDetails);
+ await test_driver.bless();
+ const p1 = request.show();
+ await test_driver.bless();
+ const p2 = request.show();
+ await test_driver.bless();
+ const p3 = request.show();
+ await request.abort();
+
+ const promises = new Set([p1, p2, p3]);
+ assert_equals(promises.size, 3, "Must have three unique objects");
+
+ await promise_rejects_dom(t, "AbortError", p1);
+ await promise_rejects_dom(t, "InvalidStateError", p2);
+ await promise_rejects_dom(t, "InvalidStateError", p3);
+}, "Calling show() multiple times always returns a new promise.");
+</script>
+<small>
+ If you find a buggy test, please
+ <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a> and
+ tag one of the
+ <a
+ href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml"
+ >suggested reviewers</a
+ >.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/complete-method-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/complete-method-manual.https.html
new file mode 100644
index 0000000000..f7facd7980
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/complete-method-manual.https.html
@@ -0,0 +1,101 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-complete()">
+<title>
+ PaymentResponse.prototype.complete() method
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="helpers.js"></script>
+<script>
+async function runManualTest({ completeWith: result }, button) {
+ button.disabled = true;
+ const { response, request } = await getPaymentRequestResponse();
+ promise_test(async t => {
+ let completePromise;
+ let invalidComplete;
+ let afterComplete;
+ try {
+ // We .complete() as normal, using the passed test value
+ completePromise = response.complete(result);
+ assert_true(completePromise instanceof Promise, "returns a promise");
+ // Immediately calling complete() again yields a rejected promise.
+ invalidComplete = response.complete(result);
+ await promise_rejects_dom(t, "InvalidStateError", invalidComplete);
+ // but the original promise is unaffected
+ const returnedValue = await completePromise;
+ assert_equals(
+ returnedValue,
+ undefined,
+ "Returned value must always be undefined"
+ );
+ // We now call .complete() again, to force an exception
+ // because [[complete]] is true.
+ afterComplete = response.complete(result);
+ await promise_rejects_dom(t, "InvalidStateError", afterComplete);
+ button.innerHTML = `✅ ${button.textContent}`;
+ } catch (err) {
+ button.innerHTML = `❌ ${button.textContent}`;
+ assert_unreached("Unexpected exception: " + err.message);
+ }
+ const allPromises = new Set([
+ completePromise,
+ invalidComplete,
+ afterComplete,
+ ]);
+ assert_equals(
+ allPromises.size,
+ 3,
+ "Calling complete() multiple times is always a new object."
+ );
+ }, button.textContent.trim());
+}
+</script>
+
+<h2>
+ Manual Tests for PaymentResponse.complete() - Please run in order!
+</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When presented with the payment sheet, use any credit card select to "Pay".
+ Also confirm any prompts that come up.
+</p>
+<ol>
+ <li>
+ <button onclick="runManualTest({completeWith: 'success'}, this)">
+ If the value of the internal slot [[completeCalled]] is true,
+ reject promise with an "InvalidStateError" DOMException.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest({completeWith: undefined}, this)">
+ Passing no argument defaults to "unknown",
+ eventually closing the sheet and doesn't throw.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest({completeWith: 'success'}, this)">
+ Passing "success" eventually closes the sheet and doesn't throw.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest({completeWith: 'fail'}, this)">
+ Passing "fail" eventually closes the sheet and doesn't throw.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest({completeWith: 'unknown'}, this)">
+ Passing "unknown" eventually closes the sheet and doesn't throw.
+ </button>
+ </li>
+ <li>
+ <button onclick="done()">Done!</button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/helpers.js b/testing/web-platform/tests/payment-request/payment-response/helpers.js
new file mode 100644
index 0000000000..1242ecb743
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/helpers.js
@@ -0,0 +1,110 @@
+setup({ explicit_done: true, explicit_timeout: true });
+
+const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ }
+});
+
+const validMethod = Object.freeze({
+ supportedMethods: "basic-card",
+});
+
+const validMethods = Object.freeze([validMethod, applePay]);
+
+const validAmount = Object.freeze({
+ currency: "USD",
+ value: "1.00",
+});
+
+const validTotal = Object.freeze({
+ label: "Valid total",
+ amount: validAmount,
+});
+const validDetails = {
+ total: validTotal,
+};
+
+test(() => {
+ try {
+ new PaymentRequest(validMethods, validDetails);
+ } catch (err) {
+ done();
+ throw err;
+ }
+}, "Can construct a payment request (smoke test).");
+
+/**
+ * Pops up a payment sheet, allowing options to be
+ * passed in if particular values are needed.
+ *
+ * @param PaymentOptions options
+ */
+async function getPaymentResponse(options, id) {
+ const { response } = await getPaymentRequestResponse(options, id);
+ return response;
+}
+
+/**
+ * Creates a payment request and response pair.
+ * It also shows the payment sheet.
+ *
+ * @param {PaymentOptions?} options
+ * @param {String?} id
+ */
+async function getPaymentRequestResponse(options, id) {
+ const methods = [{ supportedMethods: "basic-card" }];
+ const details = {
+ id,
+ total: {
+ label: "Total due",
+ amount: { currency: "USD", value: "1.0" },
+ },
+ };
+ const request = new PaymentRequest(methods, details, options);
+ const response = await request.show();
+ return { request, response };
+}
+
+/**
+ * Runs a manual test for payment
+ *
+ * @param {HTMLButtonElement} button The HTML button.
+ * @param {PaymentOptions?} options.
+ * @param {Object} expected What property values are expected to pass the test.
+ * @param {String?} id And id for the request/response pair.
+ */
+async function runManualTest(button, options, expected = {}, id = undefined) {
+ button.disabled = true;
+ const { request, response } = await getPaymentRequestResponse(options, id);
+ await response.complete();
+ test(() => {
+ assert_idl_attribute(
+ response,
+ "requestId",
+ "Expected requestId to be an IDL attribute."
+ );
+ assert_equals(response.requestId, request.id, `Expected ids to match`);
+ for (const [attribute, value] of Object.entries(expected)) {
+ assert_idl_attribute(
+ response,
+ attribute,
+ `Expected ${attribute} to be an IDL attribute.`
+ );
+ assert_equals(
+ response[attribute],
+ value,
+ `Expected response ${attribute} attribute to be ${value}`
+ );
+ }
+ assert_idl_attribute(response, "details");
+ assert_equals(typeof response.details, "object", "Expected an object");
+ // Testing that this does not throw:
+ response.toJSON();
+ }, button.textContent.trim());
+}
diff --git a/testing/web-platform/tests/payment-request/payment-response/methodName-attribute-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/methodName-attribute-manual.https.html
new file mode 100644
index 0000000000..0a8ef6c77e
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/methodName-attribute-manual.https.html
@@ -0,0 +1,28 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-methodname">
+<title>
+ PaymentResponse.prototype.methodName attribute
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="helpers.js"></script>
+<h2>methodName attribute</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ Use any credit card and any values.
+</p>
+<ol>
+ <li>
+ <button onclick="runManualTest(this, {}, { methodName: 'basic-card' }).then(done)">
+ Expect the payment method identifier to be 'basic-card'.
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute-manual.https.html
new file mode 100644
index 0000000000..5731952c0e
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute-manual.https.html
@@ -0,0 +1,73 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>PaymentResponse.prototype.onpayerdetailchange attribute</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="helpers.js"></script>
+<script>
+function runTest(button, options, expected){
+ button.disabled = true;
+ promise_test(async () => {
+ const response = await getPaymentResponse(options);
+ const eventPromise = new Promise(resolve => {
+ response.addEventListener("payerdetailchange", resolve);
+ });
+ const error = button.previousElementSibling.textContent.trim();
+ await response.retry({ error });
+ const event = await eventPromise;
+ assert_true(event instanceof PaymentRequestUpdateEvent);
+ for([prop, value] of Object.entries(expected)){
+ if (prop === 'payerPhone') {
+ // |payerPhone| may optionally adhere to E164 structure, which does not
+ // contain formatting, e.g. +180000000 instead of +1-800-000-0000.
+ // Strip out the formatting in case the user agent implements E164.
+ // https://w3c.github.io/payment-request/#addressinit-dictionary
+ value = value.replace(/[-\(\) ]/g, '');
+ }
+ assert_equals(response[prop], value);
+ }
+ await response.complete("success");
+ }, button.textContent.trim());
+}
+</script>
+<h2>Handling PaymentResponse.prototype.onpayerdetailchange events</h2>
+<p>
+ Each button will bring up the Payment Request UI window.
+ When shown the payment sheet, use any details and hit pay.
+</p>
+<p>
+ When asked to retry the payment:
+</p>
+<ol>
+ <li>
+ <p>
+ Change payer's name to "pass".
+ </p>
+ <button onclick="runTest(this, { requestPayerName: true }, { payerName: 'pass' });">
+ PaymentRequestUpdateEvent is dispatched when payer name changes.
+ </button>
+ </li>
+ <li>
+ <p>
+ Change payer's email to "pass@pass.pass".
+ </p>
+ <button onclick="runTest(this, {requestPayerEmail: true}, { payerEmail: 'pass@pass.pass' });">
+ PaymentRequestUpdateEvent is dispatched when payer email changes.
+ </button>
+ </li>
+ <li>
+ <p>
+ Change payer's phone to "+1-800-000-0000".
+ </p>
+ <button onclick="runTest(this, {requestPayerPhone: true}, { payerPhone: '+18000000000' })">
+ PaymentRequestUpdateEvent is dispatched when payer phone changes.
+ </button>
+ </li>
+ <li>
+ <button onclick="done();">DONE!</button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">owners</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute.https.html b/testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute.https.html
new file mode 100644
index 0000000000..ed9e6e885b
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/onpayerdetailchange-attribute.https.html
@@ -0,0 +1,14 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>PaymentResponse.prototype.onpayerdetailschange attribute</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+test(() => {
+ assert_equals(Object.getPrototypeOf(PaymentResponse), window.EventTarget);
+}, "PaymentResponse inherits from EventTarget");
+
+test(() => {
+ assert_true("onpayerdetailchange" in PaymentResponse.prototype);
+}, "PaymentResponse has an onpayerdetailchange in the prototype chain");
+</script>
diff --git a/testing/web-platform/tests/payment-request/payment-response/payerEmail-attribute-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/payerEmail-attribute-manual.https.html
new file mode 100644
index 0000000000..28ce4c28a8
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/payerEmail-attribute-manual.https.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-payeremail">
+<title>
+ PaymentResponse.prototype.payerEmail attribute
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="helpers.js"></script>
+<h2>payerEmail attribute</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When requested, please use "wpt@w3.org" as the email.
+</p>
+<ol>
+ <li>
+ <button onclick="runManualTest(this, undefined, { payerEmail: null })">
+ payerEmail attribute is null when options undefined.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerEmail: undefined }, { payerEmail: null })">
+ payerEmail attribute is null when requestPayerEmail is undefined.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerEmail: false }, { payerEmail: null })">
+ payerEmail attribute is null when requestPayerEmail is false.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerEmail: true }, { payerEmail: 'wpt@w3.org' })">
+ payerEmail attribute is 'wpt@w3.org' when requestPayerEmail is true.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerEmail: 'yep' }, { payerEmail: 'wpt@w3.org' }).then(done)">
+ payerEmail attribute is 'wpt@w3.org' when requestPayerEmail is truthy.
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/payerName-attribute-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/payerName-attribute-manual.https.html
new file mode 100644
index 0000000000..44d741ae45
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/payerName-attribute-manual.https.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-payername">
+<title>
+ PaymentResponse.prototype.payerName attribute
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="helpers.js"></script>
+<h2>payerName attribute</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When requested, please use "web platform test" as the payer name.
+</p>
+<ol>
+ <li>
+ <button onclick="runManualTest(this, undefined, { payerName: null })">
+ payerName attribute is null when option is undefined.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerName: undefined }, { payerName: null })">
+ payerName attribute is null when requestPayerName is undefined.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerName: false }, { payerName: null })">
+ payerName attribute is null when requestPayerName is false.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerName: true }, { payerName: 'web platform test' })">
+ payerName attribute is 'web platform test' when requestPayerName is true.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerName: 'yep' }, { payerName: 'web platform test' }).then(done)">
+ payerName attribute is 'web platform test' when requestPayerName is truthy.
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/payerPhone-attribute-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/payerPhone-attribute-manual.https.html
new file mode 100644
index 0000000000..85a44a819c
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/payerPhone-attribute-manual.https.html
@@ -0,0 +1,48 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-payerphone">
+<title>
+ PaymentResponse.prototype.payerPhone attribute
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="helpers.js"></script>
+<h2>payerPhone attribute</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When prompted, please use +12345678910 as the phone number.
+</p>
+<ol>
+ <li>
+ <button onclick="runManualTest(this, undefined, { payerPhone: null })">
+ payerPhone attribute is null when options is undefined.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerPhone: undefined }, { payerPhone: null })">
+ payerPhone attribute is null when requestPayerPhone is undefined.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerPhone: false }, { payerPhone: null })">
+ payerPhone attribute is null when requestPayerPhone is false.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerPhone: true }, { payerPhone: '+12345678910' })">
+ payerPhone attribute is '+12345678910' when requestPayerPhone is true.
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, { requestPayerPhone: 'yep' }, { payerPhone: '+12345678910' }).then(done)">
+ payerPhone attribute is '+12345678910' when requestPayerPhone is truthy.
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-immediate-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-immediate-manual.https.html
new file mode 100644
index 0000000000..7e35d78700
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-immediate-manual.https.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Dispatching PaymentRequestUpdateEvent for "payerdetailschange"</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="helpers.js"></script>
+<script>
+function testImmediateUpdate({ textContent: testName }) {
+ promise_test(async t => {
+ const response = await getPaymentResponse({ requestPayerName: true });
+ const eventPromise = new Promise((resolve, reject) => {
+ response.addEventListener(
+ "payerdetailchange",
+ ev => {
+ // Forces updateWith() to be run in the next event loop tick so that
+ // [[waitForUpdate]] is already true when it runs.
+ t.step_timeout(() => {
+ try {
+ ev.updateWith({});
+ resolve(); // This is bad.
+ } catch (err) {
+ reject(err); // this is good.
+ }
+ });
+ },
+ { once: true }
+ );
+ });
+
+ const retryPromise = response.retry({
+ payer: { name: "Change me!" },
+ });
+ await promise_rejects_dom(
+ t,
+ "InvalidStateError",
+ eventPromise,
+ "The event loop already spun, so [[waitForUpdate]] is now true"
+ );
+ await retryPromise;
+ await response.complete("success");
+ }, testName.trim());
+}
+</script>
+<h2>Handling PaymentResponse.prototype.onpayerdetailchange events</h2>
+<p>
+ The test brings up the Payment Request UI window.
+ When shown the payment sheet, use any details and hit pay.
+</p>
+<p>
+ When asked to retry the payment:
+</p>
+<ol>
+ <li>
+ <p>
+ Change payer's name to anything.
+ </p>
+ <button onclick="testImmediateUpdate(this);">
+ updateWith() must be called immediately, otherwise must throw an InvalidStateError.
+ </button>
+ </li>
+ <li>
+ <button onclick="done();">DONE!</button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">owners</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-manual.https.html
new file mode 100644
index 0000000000..1a7342365d
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/payerdetailschange-updateWith-manual.https.html
@@ -0,0 +1,56 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Dispatching PaymentRequestUpdateEvent for "payerdetailschange"</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="helpers.js"></script>
+<script>
+function runTest(button) {
+ button.disabled = true;
+ promise_test(async t => {
+ const response = await getPaymentResponse({ requestPayerName: true });
+ const eventPromise = new Promise((_, reject) => {
+ response.addEventListener("payerdetailchange", ev => {
+ // [[waitForUpdate]] becomes true...
+ ev.updateWith({});
+ // So calling it again throws "InvalidStateError".
+ try {
+ ev.updateWith({});
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ await response.retry({
+ payer: { name: "Change me!" },
+ });
+ await promise_rejects_dom(t, "InvalidStateError", eventPromise);
+ await response.complete("success");
+ }, button.textContent.trim());
+}
+</script>
+<h2>Handling PaymentResponse.prototype.onpayerdetailchange events</h2>
+<p>
+ The test brings up the Payment Request UI window.
+ When shown the payment sheet, use any details and hit pay.
+</p>
+<p>
+ When asked to retry the payment:
+</p>
+<ol>
+ <li>
+ <p>
+ Change payer's name to anything.
+ </p>
+ <button onclick="runTest(this);">
+ Calling PaymentRequestUpdateEvent updateWith() twice throws an "InvalidStateError".
+ </button>
+ </li>
+ <li>
+ <button onclick="done();">DONE!</button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">owners</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/rejects_if_not_active-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/rejects_if_not_active-manual.https.html
new file mode 100644
index 0000000000..6f2e9e95d4
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/rejects_if_not_active-manual.https.html
@@ -0,0 +1,160 @@
+<!DOCTYPE html>
+<meta charset=utf-8>
+<link rel="help" href="https://w3c.github.io/payment-request/#retry-method">
+<title>PaymentResponse retry() rejects if doc is not fully active</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-retry">
+<body>
+<script>
+setup({ explicit_done: true, explicit_timeout: true });
+const validMethod = Object.freeze({
+ supportedMethods: "basic-card",
+});
+const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ }
+});
+const validMethods = Object.freeze([validMethod, applePay]);
+const validAmount = Object.freeze({
+ currency: "USD",
+ value: "5.00",
+});
+const validTotal = Object.freeze({
+ label: "Total due",
+ amount: validAmount,
+});
+const validDetails = Object.freeze({
+ total: validTotal,
+});
+
+function getLoadedPaymentResponse(iframe, url) {
+ return new Promise(resolve => {
+ iframe.addEventListener(
+ "load",
+ async () => {
+ const { PaymentRequest } = iframe.contentWindow;
+ const response = await new PaymentRequest(
+ validMethods,
+ validDetails
+ ).show();
+ resolve(response);
+ },
+ { once: true }
+ );
+ iframe.src = url;
+ });
+}
+
+function methodNotFullyActive(button, method, ...args) {
+ const text = button.textContent.trim();
+ promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.allow = "payment";
+ document.body.appendChild(iframe);
+
+ // We first got to page1.html, grab a PaymentResponse instance.
+ const response = await getLoadedPaymentResponse(
+ iframe,
+ "/payment-request/resources/page1.html"
+ );
+ // We navigate the iframe again, putting response's document into an inactive state.
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve);
+ iframe.src = "/payment-request/resources/page2.html";
+ });
+ // Now, response's relevant global object's document is no longer active.
+ // So, promise needs to reject appropriately.
+ const promise = response[methodName](...args);
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ promise,
+ "Inactive document, so must throw AbortError"
+ );
+ // We are done, so clean up.
+ iframe.remove();
+ }, text);
+}
+
+function methodBecomesNotFullyActive(button, methodName, ...args) {
+ const text = button.textContent.trim();
+ promise_test(async t => {
+ const iframe = document.createElement("iframe");
+ iframe.allow = "payment";
+ document.body.appendChild(iframe);
+
+ // We first got to page1.html, grab a PaymentResponse instance.
+ const response = await getLoadedPaymentResponse(
+ iframe,
+ "/payment-request/resources/page1.html"
+ );
+
+ // we get the promise from page1.html, while it's active!
+ const promise = response[methodName](...args);
+
+ // We navigate the iframe again, putting response's document into an inactive state.
+ await new Promise(resolve => {
+ iframe.addEventListener("load", resolve);
+ iframe.src = "/payment-request/resources/page2.html";
+ });
+
+ // Now, response's relevant global object's document is no longer active.
+ // So, promise needs to reject appropriately.
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ promise,
+ "Inactive document, so must throw AbortError"
+ );
+ // We are done, so clean up.
+ iframe.remove();
+ }, text);
+}
+</script>
+<section>
+ <p>
+ For each test, when the payment sheet is shown, select a payment method and hit "Pay".
+ </p>
+ <h2>retry() and document active state</h2>
+ <p>Manual Tests for PaymentResponse.retry() - Please run in order!</p>
+ <ol>
+ <li>
+ <button onclick="methodNotFullyActive(this, 'retry', {});">
+ retry()'s retryPromise rejects if document is not fully active.
+ </button>
+ </li>
+ <li>
+ <button onclick="methodBecomesNotFullyActive(this, 'retry', {});">
+ retry()'s retryPromise rejects if the document becomes not fully active.
+ </button>
+ </li>
+ </ol>
+ <h2>complete() and document active state</h2>
+ <p>Manual Tests for PaymentResponse.complete() - Please run in order!</p>
+ <ol>
+ <li>
+ <button onclick="methodNotFullyActive(this, 'complete', 'success');">
+ complete()'s completePromise rejects if document is not fully active.
+ </button>
+ </li>
+ <li>
+ <button onclick="methodBecomesNotFullyActive(this, 'complete', 'success');">
+ complete()'s completePromise rejects if the document becomes not fully active.
+ </button>
+ </li>
+ <li>
+ <button onclick="done();">Done</button>
+ </li>
+ </ol>
+</section>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/payment-response/requestId-attribute-manual.https.html b/testing/web-platform/tests/payment-request/payment-response/requestId-attribute-manual.https.html
new file mode 100644
index 0000000000..ddb1e0d831
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/payment-response/requestId-attribute-manual.https.html
@@ -0,0 +1,34 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#dom-paymentresponse-requestid">
+<title>
+ PaymentResponse.prototype.requestId attribute
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="helpers.js"></script>
+<h2>requestId attribute</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When presented with the payment sheet, use any credit card select to "Pay".
+ Also confirm any prompts that come up.
+</p>
+<ol>
+ <li>
+ <button onclick="runManualTest(this, {}, {})">
+ Must mirror the payment request's automatically set id
+ </button>
+ </li>
+ <li>
+ <button onclick="runManualTest(this, {}, {requestId: 'pass'}, 'pass').then(done)">
+ Must mirror the payment request's explicitly set id
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>
diff --git a/testing/web-platform/tests/payment-request/rejects_if_not_active.https.html b/testing/web-platform/tests/payment-request/rejects_if_not_active.https.html
new file mode 100644
index 0000000000..32feccb265
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/rejects_if_not_active.https.html
@@ -0,0 +1,142 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>PaymentRequest show() rejects if doc is not fully active</title>
+<link rel="help" href="https://w3c.github.io/payment-request/#show-method" />
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<body>
+ <script>
+ const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+ });
+ const validMethod = Object.freeze({
+ supportedMethods: "basic-card",
+ });
+ const validMethods = Object.freeze([validMethod, applePay]);
+
+ const validDetails = Object.freeze({
+ total: {
+ label: "Total due",
+ amount: {
+ currency: "USD",
+ value: "5.00",
+ },
+ },
+ });
+
+ async function getLoadedPaymentRequest(iframe, url) {
+ await new Promise((resolve) => {
+ iframe.addEventListener("load", resolve, { once: true });
+ iframe.src = url;
+ });
+ return new iframe.contentWindow.PaymentRequest(
+ validMethods,
+ validDetails
+ );
+ }
+
+ promise_test(async (t) => {
+ const iframe = document.createElement("iframe");
+ iframe.allow = "payment";
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => {
+ iframe.remove();
+ });
+ // We first got to page1.html, grab a PaymentRequest instance.
+ const request1 = await getLoadedPaymentRequest(
+ iframe,
+ "./resources/page1.html"
+ );
+ // Save the DOMException of page1.html before navigating away.
+ const frameDOMException1 = iframe.contentWindow.DOMException;
+
+ // Get transient activation
+ await test_driver.bless("payment 1", () => {}, iframe.contentWindow);
+
+ // We navigate the iframe again, putting request1's document into an non-fully active state.
+ const request2 = await getLoadedPaymentRequest(
+ iframe,
+ "./resources/page2.html"
+ );
+
+ // Now, request1's relevant global object's document is no longer active.
+ // So, call .show(), and make sure it rejects appropriately.
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ frameDOMException1,
+ request1.show(),
+ "Inactive document, so must throw AbortError"
+ );
+ // request2 has an active document tho, so confirm it's working as expected:
+ // Get transient activation
+ await test_driver.bless("payment 2", () => {}, iframe.contentWindow);
+ request2.show();
+ await request2.abort();
+ await promise_rejects_dom(
+ t,
+ "InvalidStateError",
+ iframe.contentWindow.DOMException,
+ request2.show(),
+ "Abort already called, so InvalidStateError"
+ );
+ }, "PaymentRequest.show() aborts if the document is not active.");
+
+ promise_test(async (t) => {
+ // We nest two iframes and wait for them to load.
+ const outerIframe = document.createElement("iframe");
+ outerIframe.allow = "payment";
+ document.body.appendChild(outerIframe);
+ t.add_cleanup(() => {
+ outerIframe.remove();
+ });
+ // Load the outer iframe (we don't care about the awaited request)
+ await getLoadedPaymentRequest(outerIframe, "./resources/page1.html");
+
+ // Now we create the inner iframe
+ const innerIframe = outerIframe.contentDocument.createElement("iframe");
+ innerIframe.allow = "payment";
+
+ // nest them
+ outerIframe.contentDocument.body.appendChild(innerIframe);
+
+ // load innerIframe, and get the PaymentRequest instance
+ const request = await getLoadedPaymentRequest(
+ innerIframe,
+ "./resources/page2.html"
+ );
+ // Save DOMException from innerIframe before navigating away.
+ const innerIframeDOMException = innerIframe.contentWindow.DOMException;
+
+ // Navigate the outer iframe to a new location.
+ // Wait for the load event to fire.
+ await new Promise((resolve) => {
+ outerIframe.addEventListener("load", resolve);
+ outerIframe.src = "./resources/page2.html";
+ });
+
+ await test_driver.bless("", () => {}, innerIframe.contentWindow);
+ const showPromise = request.show();
+ // Now, request's relevant global object's document is still active
+ // (it is the active document of the inner iframe), but is not fully active
+ // (since the parent of the inner iframe is itself no longer active).
+ // So, call request.show() and make sure it rejects appropriately.
+ await promise_rejects_dom(
+ t,
+ "AbortError",
+ innerIframeDOMException,
+ showPromise,
+ "Active, but not fully active, so must throw AbortError"
+ );
+ }, "PaymentRequest.show() aborts if the document is active, but not fully active.");
+ </script>
+</body>
diff --git a/testing/web-platform/tests/payment-request/resources/delegate-request-subframe.sub.html b/testing/web-platform/tests/payment-request/resources/delegate-request-subframe.sub.html
new file mode 100644
index 0000000000..aeda1f00d4
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/resources/delegate-request-subframe.sub.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<title>Payment request delegation test: subframe</title>
+
+<script>
+ function reportResult(msg) {
+ window.top.postMessage({"type": "result", "result": msg}, "*");
+ }
+
+ async function requestPayment(e) {
+ const supportedMethods = [{
+ supportedMethods: "https://{{hosts[][nonexistent]}}/payment-request"
+ }];
+ const details = {
+ total: { label: "Test", amount: { currency: "CAD", value: "1.00" } }
+ };
+ const request = new PaymentRequest(supportedMethods, details);
+
+ request.show().catch(e => {
+ if (e.name == "SecurityError") {
+ reportResult("failure");
+ } else if (e.name == "NotSupportedError") {
+ // Because we used a non-existent url in supportedMethod aboves, this error message
+ // means all checks required for this test (i.e. user activation check and payment
+ // delegation check) have passed successfully.
+ reportResult("success");
+ } else {
+ reportResult("unexpected");
+ }
+ });
+ }
+
+ window.addEventListener("message", e => {
+ if (e.data.type == "make-payment-request")
+ requestPayment();
+ });
+
+ window.top.postMessage({"type": "subframe-loaded"}, "*");
+</script>
diff --git a/testing/web-platform/tests/payment-request/resources/page1.html b/testing/web-platform/tests/payment-request/resources/page1.html
new file mode 100644
index 0000000000..7fc080d380
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/resources/page1.html
@@ -0,0 +1 @@
+<meta charset="utf-8">
diff --git a/testing/web-platform/tests/payment-request/resources/page2.html b/testing/web-platform/tests/payment-request/resources/page2.html
new file mode 100644
index 0000000000..7fc080d380
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/resources/page2.html
@@ -0,0 +1 @@
+<meta charset="utf-8">
diff --git a/testing/web-platform/tests/payment-request/show-consume-activation.https.html b/testing/web-platform/tests/payment-request/show-consume-activation.https.html
new file mode 100644
index 0000000000..6f629489e4
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/show-consume-activation.https.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ <title>show() consumes user activation</title>
+ <link rel="help" href="https://w3c.github.io/payment-request/#show-method" />
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <script src="/resources/testdriver.js"></script>
+ <script src="/resources/testdriver-vendor.js"></script>
+ </head>
+ <body>
+ <script>
+ const defaultMethods = [
+ { supportedMethods: "basic-card" },
+ {
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+ },
+ ];
+
+ const defaultDetails = {
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+ };
+ promise_test(async t => {
+ const pr = new PaymentRequest(defaultMethods, defaultDetails);
+
+ await test_driver.bless("Calls show() method");
+ const showPromise = pr.show();
+
+ // The activation has been consumed.
+ assert_false(navigator.userActivation.isActive);
+
+ // Abort the payment request
+ pr.abort()
+ await promise_rejects_dom(t, "AbortError", showPromise);
+ }, "Calling show consumes user activation, if present");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/payment-request/show-method-optional-promise-rejects.https.html b/testing/web-platform/tests/payment-request/show-method-optional-promise-rejects.https.html
new file mode 100644
index 0000000000..4a41f28fc9
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/show-method-optional-promise-rejects.https.html
@@ -0,0 +1,215 @@
+<!DOCTYPE html>
+<meta charset="utf-8" />
+<title>Test for PaymentRequest.show(optional detailsPromise) method</title>
+<link
+ rel="help"
+ href="https://w3c.github.io/browser-payment-api/#show-method"
+/>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/resources/testdriver.js"></script>
+<script src="/resources/testdriver-vendor.js"></script>
+<script>
+ // See function testBadUpdate() for test details!
+ setup({
+ allow_uncaught_exception: true,
+ });
+
+ // == TEST DATA ===
+ // PaymentMethod
+ const validMethod = Object.freeze({
+ supportedMethods: "valid-but-wont-ever-match",
+ });
+
+ const validMethodBasicCard = Object.freeze({
+ supportedMethods: "basic-card",
+ });
+
+ const validMethodApplePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ },
+ });
+
+ // Methods
+ const validMethods = Object.freeze([
+ validMethodBasicCard,
+ validMethod,
+ validMethodApplePay,
+ ]);
+
+ // Amounts
+ const validAmount = Object.freeze({
+ currency: "USD",
+ value: "1.00",
+ });
+
+ const invalidAmount = Object.freeze({
+ currency: "¡INVALID!",
+ value: "A1.0",
+ });
+
+ const negativeAmount = Object.freeze({
+ currency: "USD",
+ value: "-1.00",
+ });
+
+ // Totals
+ const validTotal = Object.freeze({
+ label: "Valid Total",
+ amount: validAmount,
+ });
+
+ const invalidTotal = Object.freeze({
+ label: "Invalid Total",
+ amount: invalidAmount,
+ });
+
+ const invalidNegativeTotal = Object.freeze({
+ label: "Invalid negative total",
+ amount: negativeAmount,
+ });
+
+ // PaymentDetailsInit
+ const validDetails = Object.freeze({
+ total: validTotal,
+ });
+
+ const invalidDetailsNegativeTotal = Object.freeze({
+ total: invalidNegativeTotal,
+ });
+
+
+ // PaymentItem
+ const validPaymentItem = Object.freeze({
+ amount: validAmount,
+ label: "Valid payment item",
+ });
+
+ const invalidPaymentItem = Object.freeze({
+ amount: invalidAmount,
+ label: "Invalid payment item",
+ });
+
+ // PaymentItem
+ const validPaymentItems = Object.freeze([validPaymentItem]);
+ const invalidPaymentItems = Object.freeze([invalidPaymentItem]);
+
+ // PaymentDetailsModifier
+ const validModifier = Object.freeze({
+ additionalDisplayItems: validPaymentItems,
+ supportedMethods: "valid-but-wont-ever-match",
+ total: validTotal,
+ });
+
+ const modifierWithInvalidDisplayItems = Object.freeze({
+ additionalDisplayItems: invalidPaymentItems,
+ supportedMethods: "basic-card",
+ total: validTotal,
+ });
+
+ const modifierWithValidDisplayItems = Object.freeze({
+ additionalDisplayItems: validPaymentItems,
+ supportedMethods: "basic-card",
+ total: validTotal,
+ });
+
+ const modifierWithInvalidTotal = Object.freeze({
+ additionalDisplayItems: validPaymentItems,
+ supportedMethods: "basic-card",
+ total: invalidTotal,
+ });
+
+ const recursiveData = {};
+ recursiveData.foo = recursiveData;
+ Object.freeze(recursiveData);
+
+ const modifierWithRecursiveData = Object.freeze({
+ supportedMethods: "basic-card",
+ total: validTotal,
+ data: recursiveData,
+ });
+ // == END OF TEST DATA ===
+ /*
+ These test work by creating a "valid" payment request and then
+ performing a bad update via `show(detailsPromise)`.
+ The `badDetails` cause detailsPromise to reject with `expectedError`.
+ */
+ function testBadUpdate(testAssertion, badDetails, expectedError) {
+ promise_test(async (t) => {
+ const request = new PaymentRequest(
+ validMethods,
+ validDetails
+ );
+ await test_driver.bless("Payment request");
+ const detailsPromise = Promise.resolve(badDetails);
+ const acceptPromise = request.show(detailsPromise);
+ const test_func =
+ typeof expectedError == "function"
+ ? promise_rejects_js
+ : promise_rejects_dom;
+ await test_func(
+ t,
+ expectedError,
+ acceptPromise,
+ "badDetails must cause acceptPromise to reject with expectedError"
+ );
+ }, testAssertion);
+ }
+
+ const rejectedPromise = Promise.reject(new SyntaxError("test"));
+ testBadUpdate(
+ "Rejection of detailsPromise must abort the update with an 'AbortError' DOMException.",
+ rejectedPromise,
+ "AbortError"
+ );
+
+ testBadUpdate(
+ "Total in the update is a string, so converting to IDL must abort the update with a TypeError.",
+ { total: `this will cause a TypeError!` },
+ TypeError
+ );
+
+ testBadUpdate(
+ "Total is recursive, so converting to IDL must abort the update with a TypeError.",
+ { total: recursiveData },
+ TypeError
+ );
+
+ testBadUpdate(
+ "Updating with a negative total results in a TypeError.",
+ invalidDetailsNegativeTotal,
+ TypeError
+ );
+
+ testBadUpdate(
+ "Updating with a displayItem with an invalid currency results in RangeError.",
+ { ...validDetails, displayItems: invalidPaymentItems },
+ RangeError
+ );
+
+ testBadUpdate(
+ "Must throw a RangeError when a modifier's total item has an invalid currency.",
+ { ...validDetails, modifiers: [modifierWithInvalidTotal, validModifier] },
+ RangeError
+ );
+
+ testBadUpdate(
+ "Must throw a RangeError when a modifier display item has an invalid currency.",
+ {
+ ...validDetails,
+ modifiers: [modifierWithInvalidDisplayItems, validModifier],
+ },
+ RangeError
+ );
+ testBadUpdate(
+ "Must throw as Modifier has a recursive dictionary.",
+ { ...validDetails, modifiers: [modifierWithRecursiveData, validModifier] },
+ TypeError
+ );
+</script>
diff --git a/testing/web-platform/tests/payment-request/show-method-postmessage-iframe.html b/testing/web-platform/tests/payment-request/show-method-postmessage-iframe.html
new file mode 100644
index 0000000000..b50f18ecce
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/show-method-postmessage-iframe.html
@@ -0,0 +1,49 @@
+<h1>This iframe calls shows() via postMessage()</h1>
+<script>
+"use strict";
+const defaultMethods = Object.freeze([
+ { supportedMethods: "basic-card" },
+ {
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ }
+ },
+]);
+
+const defaultDetails = Object.freeze({
+ id: "fail",
+ total: {
+ label: "Total",
+ amount: {
+ currency: "USD",
+ value: "1.00",
+ },
+ },
+});
+
+// We are going to use the id to prove that this works
+// which we will pass back to the caller
+window.onmessage = async event => {
+ const { source, data: { id, request } } = event;
+ switch (request) {
+ case "show-payment-request": {
+ const details = Object.assign({}, defaultDetails, { id });
+ const request = new PaymentRequest(defaultMethods, details);
+ try {
+ const response = await request.show();
+ source.postMessage(response.toJSON(), window.location.origin);
+ await response.complete();
+ } catch (err) {
+ source.postMessage({ requestId: "fail" }, window.location.origin);
+ await request.abort();
+ }
+ }
+ }
+};
+
+</script>
diff --git a/testing/web-platform/tests/payment-request/show-method-postmessage-manual.https.html b/testing/web-platform/tests/payment-request/show-method-postmessage-manual.https.html
new file mode 100644
index 0000000000..0751920e37
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/show-method-postmessage-manual.https.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test for PaymentRequest.show() method</title>
+<link rel="help" href="https://w3c.github.io/browser-payment-api/#show-method">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+"use strict";
+setup({
+ explicit_done: true,
+ explicit_timeout: true,
+ allow_uncaught_exception: true,
+});
+
+async function runUserActivation(button) {
+ button.disabled = true;
+ const { contentWindow: iframeWindow } = document.getElementById("iframe");
+ const expectedId = "pass123";
+ await Promise.resolve(); // next tick
+ const promiseForResponse = new Promise(resolve => {
+ window.onmessage = ({ data: { requestId } }) => resolve(requestId);
+ });
+ const ops = { id: expectedId, request: "show-payment-request" };
+ iframeWindow.postMessage(ops, window.location.origin);
+ promise_test(async () => {
+ const actualId = await promiseForResponse;
+ assert_equals(actualId, expectedId, "ids must match");
+ }, button.textContent.trim());
+ done();
+}
+
+</script>
+<h2>Test PaymentRequest.show() triggered by user activation using postMessage()</h2>
+<p>
+ Tests that user activation works over postMessage().
+</p>
+<p>
+ Click on bottom below. Hit "Pay".
+</p>
+<ol>
+ <li>
+ <button onclick="runUserActivation(this)">
+ show() is triggered by user activation passed through postMessage() and a promise
+ </button>
+ </li>
+</ol>
+<iframe width="100%" id="iframe" src="show-method-postmessage-iframe.html" allow="payment"></iframe>
+<p>
+ <small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+ </small>
+</p>
diff --git a/testing/web-platform/tests/payment-request/user-abort-algorithm-manual.https.html b/testing/web-platform/tests/payment-request/user-abort-algorithm-manual.https.html
new file mode 100644
index 0000000000..078bf3d61a
--- /dev/null
+++ b/testing/web-platform/tests/payment-request/user-abort-algorithm-manual.https.html
@@ -0,0 +1,80 @@
+<!doctype html>
+<meta charset="utf8">
+<link rel="help" href="https://w3c.github.io/payment-request/#user-aborts-the-payment-request-algorithm">
+<title>
+ User aborts the payment request algorithm.
+</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+setup({ explicit_done: true, explicit_timeout: true });
+
+const validAmount = Object.freeze({
+ currency: "USD",
+ value: "1.0",
+});
+const validTotal = Object.freeze({
+ label: "Total due",
+ amount: validAmount,
+});
+const applePay = Object.freeze({
+ supportedMethods: "https://apple.com/apple-pay",
+ data: {
+ version: 3,
+ merchantIdentifier: "merchant.com.example",
+ countryCode: "US",
+ merchantCapabilities: ["supports3DS"],
+ supportedNetworks: ["visa"],
+ }
+});
+const validMethod = Object.freeze({
+ supportedMethods: "basic-card",
+});
+const validMethods = Object.freeze([validMethod, applePay]);
+const validDetails = Object.freeze({
+ total: validTotal,
+});
+
+test(() => {
+ try {
+ new PaymentRequest(validMethods, validDetails);
+ } catch (err) {
+ done();
+ throw err;
+ }
+}, "Can construct a payment request (smoke test).");
+
+async function runAbortTest(button) {
+ button.disabled = true;
+ const { textContent: testName } = button;
+ promise_test(async t => {
+ const request = new PaymentRequest(validMethods, validDetails);
+ // Await the user to abort
+ await promise_rejects_dom(t, "AbortError", request.show());
+ // [[state]] is now closed
+ await promise_rejects_dom(t, "InvalidStateError", request.show());
+ }, testName.trim());
+}
+</script>
+<h2>
+ User aborts the payment request algorithm.
+</h2>
+<p>
+ Click on each button in sequence from top to bottom without refreshing the page.
+ Each button will bring up the Payment Request UI window.
+</p>
+<p>
+ When presented with the payment sheet, abort the payment request
+ (e.g., by hitting the esc key or pressing a UA provided button).
+</p>
+<ol>
+ <li>
+ <button onclick="runAbortTest(this); done();">
+ If the user aborts, the UA must run the user aborts the payment request algorithm.
+ </button>
+ </li>
+</ol>
+<small>
+ If you find a buggy test, please <a href="https://github.com/web-platform-tests/wpt/issues">file a bug</a>
+ and tag one of the <a href="https://github.com/web-platform-tests/wpt/blob/master/payment-request/META.yml">suggested reviewers</a>.
+</small>