summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/fetch/fetch-later
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/fetch/fetch-later
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/fetch/fetch-later')
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/META.yml3
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/README.md3
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js53
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js46
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js6
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html23
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html19
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html25
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html23
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html24
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html24
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html24
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html24
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js63
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js75
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/non-secure.window.js5
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js26
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js31
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js33
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js128
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html14
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js39
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js183
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js23
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js30
-rw-r--r--testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js28
26 files changed, 975 insertions, 0 deletions
diff --git a/testing/web-platform/tests/fetch/fetch-later/META.yml b/testing/web-platform/tests/fetch/fetch-later/META.yml
new file mode 100644
index 0000000000..f8fd46bec3
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/META.yml
@@ -0,0 +1,3 @@
+spec: https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method
+suggested_reviewers:
+ - mingyc
diff --git a/testing/web-platform/tests/fetch/fetch-later/README.md b/testing/web-platform/tests/fetch/fetch-later/README.md
new file mode 100644
index 0000000000..661e2b9184
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/README.md
@@ -0,0 +1,3 @@
+# FetchLater Tests
+
+These tests cover [FetchLater method](https://whatpr.org/fetch/1647/094ea69...152d725.html#fetch-later-method) related behaviors.
diff --git a/testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js
new file mode 100644
index 0000000000..18b368066b
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/activate-after.tentative.https.window.js
@@ -0,0 +1,53 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Loads an iframe that creates a fetchLater request w/ short timeout.
+ const iframe = await loadScriptAsIframe(`
+ fetchLater("${url}", {activateAfter: 1000}); // 1s
+ `);
+ // Deletes the iframe to trigger deferred request sending.
+ document.body.removeChild(iframe);
+
+ // The iframe should have sent all requests.
+ await expectBeacon(uuid, {count: 1});
+}, 'fetchLater() sends out based on activateAfter.');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // Sets no option to test the default behavior when a document enters BFCache.
+ const helper = new RemoteContextHelper();
+ // Opens a window with noopener so that BFCache will work.
+ const rc1 = await helper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // Creates a fetchLater request with short timeout. It should be sent out
+ // even if the document is then put into BFCache.
+ await rc1.executeScript(url => {
+ fetchLater(url, {activateAfter: 1000}); // 1s.
+ // Add a pageshow listener to stash the BFCache event.
+ window.addEventListener('pageshow', e => {
+ window.pageshowEvent = e;
+ });
+ }, [url]);
+ // Navigates away to let page enter BFCache.
+ const rc2 = await rc1.navigateToNew();
+ // Navigate back.
+ await rc2.historyBack();
+ // Verify that the page was BFCached.
+ assert_true(await rc1.executeScript(() => {
+ return window.pageshowEvent.persisted;
+ }));
+
+ await expectBeacon(uuid, {count: 1});
+}, 'fetchLater() sends out based on activateAfter, even if document is in BFCache.');
diff --git a/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js
new file mode 100644
index 0000000000..37f72ab89e
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.window.js
@@ -0,0 +1,46 @@
+'use strict';
+
+test(() => {
+ assert_throws_js(TypeError, () => fetchLater());
+}, `fetchLater() cannot be called without request.`);
+
+test(() => {
+ assert_throws_js(TypeError, () => fetchLater('http://www.google.com'));
+ assert_throws_js(TypeError, () => fetchLater('file://tmp'));
+ assert_throws_js(TypeError, () => fetchLater('ssh://example.com'));
+ assert_throws_js(TypeError, () => fetchLater('wss://example.com'));
+ assert_throws_js(TypeError, () => fetchLater('about:blank'));
+ assert_throws_js(TypeError, () => fetchLater(`javascript:alert('');`));
+}, `fetchLater() throws TypeError on non-HTTPS URL.`);
+
+test(() => {
+ assert_throws_js(
+ RangeError,
+ () => fetchLater('https://www.google.com', {activateAfter: -1}));
+}, `fetchLater() throws RangeError on negative activateAfter.`);
+
+test(() => {
+ const result = fetchLater('/');
+ assert_false(result.activated);
+}, `fetchLater()'s return tells the deferred request is not yet sent.`);
+
+test(() => {
+ const result = fetchLater('/');
+ assert_throws_js(TypeError, () => result.activated = true);
+}, `fetchLater() throws TypeError when mutating its returned state.`);
+
+test(() => {
+ const controller = new AbortController();
+ // Immediately aborts the controller.
+ controller.abort();
+ assert_throws_dom(
+ 'AbortError', () => fetchLater('/', {signal: controller.signal}));
+}, `fetchLater() throws AbortError when its initial abort signal is aborted.`);
+
+test(() => {
+ const controller = new AbortController();
+ const result = fetchLater('/', {signal: controller.signal});
+ assert_false(result.activated);
+ controller.abort();
+ assert_false(result.activated);
+}, `fetchLater() does not throw error when it is aborted before sending.`);
diff --git a/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js
new file mode 100644
index 0000000000..17240db354
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/basic.tentative.https.worker.js
@@ -0,0 +1,6 @@
+importScripts('/resources/testharness.js');
+
+test(() => {
+ assert_false('fetchLater' in self);
+}, `fetchLater() is not supported in worker.`);
+done();
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html
new file mode 100644
index 0000000000..38eada4513
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer-when-downgrade.tentative.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: No Referrer When Downgrade Policy</title>
+<meta name='referrer' content='no-referrer-when-downgrade'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_ORIGIN,
+} = get_host_info();
+
+testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_URL);
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html
new file mode 100644
index 0000000000..75e9ece7ba
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-no-referrer.tentative.https.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: No Referrer Policy</title>
+<meta name='referrer' content='no-referrer'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+testReferrerHeader(token(), /*host=*/'', /*expectedReferer=*/"");
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html
new file mode 100644
index 0000000000..b9f14171ba
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin-when-cross-origin.tentative.https.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: Origin When Cross Origin Policy</title>
+<meta name='referrer' content='origin-when-cross-origin'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_ORIGIN,
+ HTTPS_REMOTE_ORIGIN
+} = get_host_info();
+
+testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_URL);
+testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, REFERRER_ORIGIN);
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html
new file mode 100644
index 0000000000..ce7abf9203
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-origin.tentative.https.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: Origin Policy</title>
+<meta name='referrer' content='origin'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_REMOTE_ORIGIN
+} = get_host_info();
+
+testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, REFERRER_ORIGIN);
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html
new file mode 100644
index 0000000000..264beddc03
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-same-origin.tentative.https.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: Same Origin Policy</title>
+<meta name='referrer' content='same-origin'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_REMOTE_ORIGIN
+} = get_host_info();
+
+testReferrerHeader(token(), /*host=*/'', REFERRER_URL);
+testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, /*expectedReferrer=*/'');
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html
new file mode 100644
index 0000000000..9133f2496f
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin-when-cross-origin.tentative.https.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: Strict Origin Policy</title>
+<meta name='referrer' content='strict-origin'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_REMOTE_ORIGIN
+} = get_host_info();
+
+testReferrerHeader(token(), HTTPS_REMOTE_ORIGIN, REFERRER_ORIGIN);
+// Note: FetchLater cannot be used for non-secure URL.
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html
new file mode 100644
index 0000000000..943d70bbc5
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-strict-origin.tentative.https.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: Strict Origin Policy</title>
+<meta name='referrer' content='strict-origin'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_ORIGIN
+} = get_host_info();
+
+testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_ORIGIN);
+// Note: FetchLater cannot be used for non-secure URL.
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html
new file mode 100644
index 0000000000..a602e0003a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/headers/header-referrer-unsafe-url.tentative.https.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+<title>FetchLater Referrer Header: Unsafe Url Policy</title>
+<meta name='referrer' content='unsafe-url'>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/utils.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+<script src="/fetch/fetch-later/resources/header-referrer-helper.js"></script>
+</head>
+<body>
+<script>
+
+const {
+ HTTPS_ORIGIN
+} = get_host_info();
+
+testReferrerHeader(token(), HTTPS_ORIGIN, REFERRER_URL);
+// Note: FetchLater cannot be used for non-secure URL.
+
+</script>
+</body>
+</html>
diff --git a/testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js
new file mode 100644
index 0000000000..1e9fed1117
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/iframe.tentative.https.window.js
@@ -0,0 +1,63 @@
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+const {
+ HTTPS_ORIGIN,
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+async function loadElement(el) {
+ const loaded = new Promise(resolve => el.onload = resolve);
+ document.body.appendChild(el);
+ await loaded;
+}
+
+// `host` may be cross-origin
+async function loadFetchLaterIframe(host, targetUrl) {
+ const url = `${host}/fetch/fetch-later/resources/fetch-later.html?url=${
+ encodeURIComponent(targetUrl)}`;
+ const iframe = document.createElement('iframe');
+ iframe.src = url;
+ await loadElement(iframe);
+ return iframe;
+}
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Loads a blank iframe that fires a fetchLater request.
+ const iframe = document.createElement('iframe');
+ iframe.addEventListener('load', () => {
+ fetchLater(url, {activateAfter: 0});
+ });
+ await loadElement(iframe);
+
+ // The iframe should have sent the request.
+ await expectBeacon(uuid, {count: 1});
+}, 'A blank iframe can trigger fetchLater.');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Loads a same-origin iframe that fires a fetchLater request.
+ await loadFetchLaterIframe(HTTPS_ORIGIN, url);
+
+ // The iframe should have sent the request.
+ await expectBeacon(uuid, {count: 1});
+}, 'A same-origin iframe can trigger fetchLater.');
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Loads a same-origin iframe that fires a fetchLater request.
+ await loadFetchLaterIframe(HTTPS_NOTSAMESITE_ORIGIN, url);
+
+ // The iframe should have sent the request.
+ await expectBeacon(uuid, {count: 1});
+}, 'A cross-origin iframe can trigger fetchLater.');
diff --git a/testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js
new file mode 100644
index 0000000000..93705418f2
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/new-window.tentative.https.window.js
@@ -0,0 +1,75 @@
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+const {
+ HTTPS_ORIGIN,
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+function fetchLaterPopupUrl(host, targetUrl) {
+ return `${host}/fetch/fetch-later/resources/fetch-later.html?url=${
+ encodeURIComponent(targetUrl)}`;
+}
+
+for (const target of ['', '_blank']) {
+ for (const features in ['', 'popup', 'popup,noopener']) {
+ parallelPromiseTest(
+ async t => {
+ const uuid = token();
+ const url =
+ generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN});
+
+ // Opens a blank popup window that fires a fetchLater request.
+ const w = window.open(
+ `javascript: fetchLater("${url}", {activateAfter: 0})`, target,
+ features);
+ await new Promise(resolve => w.addEventListener('load', resolve));
+
+ // The popup should have sent the request.
+ await expectBeacon(uuid, {count: 1});
+ w.close();
+ },
+ `A blank window[target='${target}'][features='${
+ features}'] can trigger fetchLater.`);
+
+ parallelPromiseTest(
+ async t => {
+ const uuid = token();
+ const popupUrl =
+ fetchLaterPopupUrl(HTTPS_ORIGIN, generateSetBeaconURL(uuid));
+
+ // Opens a same-origin popup that fires a fetchLater request.
+ const w = window.open(popupUrl, target, features);
+ await new Promise(resolve => w.addEventListener('load', resolve));
+
+ // The popup should have sent the request.
+ await expectBeacon(uuid, {count: 1});
+ w.close();
+ },
+ `A same-origin window[target='${target}'][features='${
+ features}'] can trigger fetchLater.`);
+
+ parallelPromiseTest(
+ async t => {
+ const uuid = token();
+ const popupUrl = fetchLaterPopupUrl(
+ HTTPS_NOTSAMESITE_ORIGIN, generateSetBeaconURL(uuid));
+
+ // Opens a cross-origin popup that fires a fetchLater request.
+ const w = window.open(popupUrl, target, features);
+ // As events from cross-origin window is not accessible, waiting for
+ // its message instead.
+ await new Promise(
+ resolve => window.addEventListener('message', resolve));
+
+ // The popup should have sent the request.
+ await expectBeacon(uuid, {count: 1});
+ w.close();
+ },
+ `A cross-origin window[target='${target}'][features='${
+ features}'] can trigger fetchLater.`);
+ }
+}
diff --git a/testing/web-platform/tests/fetch/fetch-later/non-secure.window.js b/testing/web-platform/tests/fetch/fetch-later/non-secure.window.js
new file mode 100644
index 0000000000..c13932e353
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/non-secure.window.js
@@ -0,0 +1,5 @@
+'use strict';
+
+test(() => {
+ assert_false(window.hasOwnProperty('fetchLater'));
+}, `fetchLater() is not supported in non-secure context.`);
diff --git a/testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js
new file mode 100644
index 0000000000..60730e0242
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/policies/csp-allowed.tentative.https.window.js
@@ -0,0 +1,26 @@
+// META: title=FetchLater: allowed by CSP
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+'use strict';
+
+const {
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+// FetchLater requests allowed by Content Security Policy.
+// https://w3c.github.io/webappsec-csp/#should-block-request
+
+const meta = document.createElement('meta');
+meta.setAttribute('http-equiv', 'Content-Security-Policy');
+meta.setAttribute('content', `connect-src 'self' ${HTTPS_NOTSAMESITE_ORIGIN}`);
+document.head.appendChild(meta);
+
+promise_test(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN});
+ fetchLater(url, {activateAfter: 0});
+
+ await expectBeacon(uuid, {count: 1});
+ t.done();
+}, 'FetchLater allowed by CSP should succeed');
diff --git a/testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js
new file mode 100644
index 0000000000..b32ddaecfc
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/policies/csp-blocked.tentative.https.window.js
@@ -0,0 +1,31 @@
+// META: title=FetchLater: blocked by CSP
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+'use strict';
+
+const {
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+// FetchLater requests blocked by Content Security Policy are rejected.
+// https://w3c.github.io/webappsec-csp/#should-block-request
+
+const meta = document.createElement('meta');
+meta.setAttribute('http-equiv', 'Content-Security-Policy');
+meta.setAttribute('content', 'connect-src \'self\'');
+document.head.appendChild(meta);
+
+promise_test(async t => {
+ const uuid = token();
+ const cspViolationUrl =
+ generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN});
+ fetchLater(cspViolationUrl, {activateAfter: 0});
+
+ await new Promise(
+ resolve => window.addEventListener('securitypolicyviolation', e => {
+ assert_equals(e.violatedDirective, 'connect-src');
+ resolve();
+ }));
+ t.done();
+}, 'FetchLater blocked by CSP should reject');
diff --git a/testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js
new file mode 100644
index 0000000000..3c18727156
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/policies/csp-redirect-to-blocked.tentative.https.window.js
@@ -0,0 +1,33 @@
+// META: title=FetchLater: redirect blocked by CSP
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+// META: timeout=long
+
+'use strict';
+
+const {
+ HTTPS_NOTSAMESITE_ORIGIN,
+} = get_host_info();
+
+// FetchLater requests redirect to URL blocked by Content Security Policy.
+// https://w3c.github.io/webappsec-csp/#should-block-request
+
+const meta = document.createElement('meta');
+meta.setAttribute('http-equiv', 'Content-Security-Policy');
+meta.setAttribute('content', 'connect-src \'self\'');
+document.head.appendChild(meta);
+
+promise_test(async t => {
+ const uuid = token();
+ const cspViolationUrl =
+ generateSetBeaconURL(uuid, {host: HTTPS_NOTSAMESITE_ORIGIN});
+ const url =
+ `/common/redirect.py?location=${encodeURIComponent(cspViolationUrl)}`;
+ fetchLater(url, {activateAfter: 0});
+
+ // TODO(crbug.com/1465781): redirect csp check is handled in browser, of which
+ // result cannot be populated to renderer at this moment.
+ await expectBeacon(uuid, {count: 0});
+ t.done();
+}, 'FetchLater redirect blocked by CSP should reject');
diff --git a/testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js
new file mode 100644
index 0000000000..1b5b85563d
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/quota.tentative.https.window.js
@@ -0,0 +1,128 @@
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+const kQuotaPerOrigin = 64 * 1024; // 64 kilobytes per spec.
+const {ORIGIN, HTTPS_NOTSAMESITE_ORIGIN} = get_host_info();
+
+// Runs a test case that cover a single fetchLater() call with `body` in its
+// request payload. The call is not expected to throw any errors.
+function fetchLaterPostTest(body, description) {
+ test(() => {
+ const controller = new AbortController();
+ const result = fetchLater(
+ '/fetch-later',
+ {method: 'POST', signal: controller.signal, body: body});
+ assert_false(result.activated);
+ // Release quota taken by the pending request for subsequent tests.
+ controller.abort();
+ }, description);
+}
+
+// Test small payload for each supported data types.
+for (const [dataType, skipCharset] of Object.entries(
+ BeaconDataTypeToSkipCharset)) {
+ fetchLaterPostTest(
+ makeBeaconData(generateSequentialData(0, 1024, skipCharset), dataType),
+ `A fetchLater() call accept small data in POST request of ${dataType}.`);
+}
+
+// Test various size of payloads for the same origin.
+for (const dataType in BeaconDataType) {
+ if (dataType !== BeaconDataType.FormData &&
+ dataType !== BeaconDataType.URLSearchParams) {
+ // Skips FormData & URLSearchParams, as browser adds extra bytes to them
+ // in addition to the user-provided content. It is difficult to test a
+ // request right at the quota limit.
+ fetchLaterPostTest(
+ // Generates data that is exactly 64 kilobytes.
+ makeBeaconData(generatePayload(kQuotaPerOrigin), dataType),
+ `A single fetchLater() call takes up the per-origin quota for its ` +
+ `body of ${dataType}.`);
+ }
+}
+
+// Test empty payload.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ assert_throws_js(
+ TypeError, () => fetchLater('/', {method: 'POST', body: ''}));
+ },
+ `A single fetchLater() call does not accept empty data in POST request ` +
+ `of ${dataType}.`);
+}
+
+// Test oversized payload.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ assert_throws_dom(
+ 'QuotaExceededError',
+ () => fetchLater('/fetch-later', {
+ method: 'POST',
+ // Generates data that exceeds 64 kilobytes.
+ body:
+ makeBeaconData(generatePayload(kQuotaPerOrigin + 1), dataType)
+ }));
+ },
+ `A single fetchLater() call is not allowed to exceed per-origin quota ` +
+ `for its body of ${dataType}.`);
+}
+
+// Test accumulated oversized request.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ const controller = new AbortController();
+ // Makes the 1st call that sends only half of allowed quota.
+ fetchLater('/fetch-later', {
+ method: 'POST',
+ signal: controller.signal,
+ body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType)
+ });
+
+ // Makes the 2nd call that sends half+1 of allowed quota.
+ assert_throws_dom('QuotaExceededError', () => {
+ fetchLater('/fetch-later', {
+ method: 'POST',
+ signal: controller.signal,
+ body: makeBeaconData(
+ generatePayload(kQuotaPerOrigin / 2 + 1), dataType)
+ });
+ });
+ // Release quota taken by the pending requests for subsequent tests.
+ controller.abort();
+ },
+ `The 2nd fetchLater() call is not allowed to exceed per-origin quota ` +
+ `for its body of ${dataType}.`);
+}
+
+// Test various size of payloads across different origins.
+for (const dataType in BeaconDataType) {
+ test(
+ () => {
+ const controller = new AbortController();
+ // Makes the 1st call that sends only half of allowed quota.
+ fetchLater('/fetch-later', {
+ method: 'POST',
+ signal: controller.signal,
+ body: makeBeaconData(generatePayload(kQuotaPerOrigin / 2), dataType)
+ });
+
+ // Makes the 2nd call that sends half+1 of allowed quota, but to a
+ // different origin.
+ fetchLater(`${HTTPS_NOTSAMESITE_ORIGIN}/fetch-later`, {
+ method: 'POST',
+ signal: controller.signal,
+ body:
+ makeBeaconData(generatePayload(kQuotaPerOrigin / 2 + 1), dataType)
+ });
+ // Release quota taken by the pending requests for subsequent tests.
+ controller.abort();
+ },
+ `The 2nd fetchLater() call to another origin does not exceed per-origin` +
+ ` quota for its body of ${dataType}.`);
+}
diff --git a/testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html b/testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html
new file mode 100644
index 0000000000..b569e1a076
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/resources/fetch-later.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<head>
+</head>
+<body>
+ <script>
+ const PARAMS = new URL(location.href).searchParams;
+ const TARGET_URL= decodeURIComponent(PARAMS.get('url')) || '';
+
+ fetchLater(TARGET_URL, {activateAfter: 0});
+ if (window.opener) {
+ window.opener.postMessage("done", "*");
+ }
+ </script>
+</body>
diff --git a/testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js b/testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js
new file mode 100644
index 0000000000..374097614a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/resources/header-referrer-helper.js
@@ -0,0 +1,39 @@
+'use strict';
+
+// https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
+const REFERRER_ORIGIN = self.location.origin + '/';
+const REFERRER_URL = self.location.href;
+
+function testReferrerHeader(id, host, expectedReferrer) {
+ const url = `${
+ host}/beacon/resources/inspect-header.py?header=referer&cmd=put&id=${id}`;
+
+ promise_test(t => {
+ fetchLater(url, {activateAfter: 0});
+ return pollResult(expectedReferrer, id).then(result => {
+ assert_equals(result, expectedReferrer, 'Correct referrer header result');
+ });
+ }, `Test referer header ${host}`);
+}
+
+function pollResult(expectedReferrer, id) {
+ const checkUrl =
+ `/beacon/resources/inspect-header.py?header=referer&cmd=get&id=${id}`;
+
+ return new Promise(resolve => {
+ function checkResult() {
+ fetch(checkUrl).then(response => {
+ assert_equals(
+ response.status, 200, 'Inspect header response\'s status is 200');
+ let result = response.headers.get('x-request-referer');
+
+ if (result != undefined) {
+ resolve(result);
+ } else {
+ step_timeout(checkResult.bind(this), 100);
+ }
+ });
+ }
+ checkResult();
+ });
+}
diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js
new file mode 100644
index 0000000000..d91c73580a
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/send-on-deactivate.tentative.https.window.js
@@ -0,0 +1,183 @@
+// META: script=/common/dispatcher/dispatcher.js
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=/html/browsers/browsing-the-web/remote-context-helper/resources/remote-context-helper.js
+// META: script=/html/browsers/browsing-the-web/back-forward-cache/resources/rc-helper.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+// NOTE: Due to the restriction of WPT runner, the following tests are all run
+// with BackgroundSync off, which is different from some browsers,
+// e.g. Chrome, default behavior, as the testing infra does not support enabling
+// it.
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // Sets no option to test the default behavior when a document enters BFCache.
+ const helper = new RemoteContextHelper();
+ // Opens a window with noopener so that BFCache will work.
+ const rc1 = await helper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // Creates a fetchLater request with default config in remote, which should
+ // only be sent on page discarded (not on entering BFCache).
+ await rc1.executeScript(url => {
+ fetchLater(url);
+ // Add a pageshow listener to stash the BFCache event.
+ window.addEventListener('pageshow', e => {
+ window.pageshowEvent = e;
+ });
+ }, [url]);
+ // Navigates away to let page enter BFCache.
+ const rc2 = await rc1.navigateToNew();
+ // Navigates back.
+ await rc2.historyBack();
+ // Verifies the page was BFCached.
+ assert_true(await rc1.executeScript(() => {
+ return window.pageshowEvent.persisted;
+ }));
+
+ // Theoretically, the request should still be pending thus 0 request received.
+ // However, 1 request is sent, as by default the WPT test runner, e.g.
+ // content_shell in Chromium, does not enable BackgroundSync permission,
+ // resulting in forcing request sending on every navigation.
+ await expectBeacon(uuid, {count: 1});
+}, `fetchLater() sends on page entering BFCache if BackgroundSync is off.`);
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const helper = new RemoteContextHelper();
+ // Opens a window with noopener so that BFCache will work.
+ const rc1 = await helper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // When the remote is put into BFCached, creates a fetchLater request w/
+ // activateAfter = 0s. It should be sent out immediately.
+ await rc1.executeScript(url => {
+ window.addEventListener('pagehide', e => {
+ if (e.persisted) {
+ fetchLater(url, {activateAfter: 0});
+ }
+ });
+ // Add a pageshow listener to stash the BFCache event.
+ window.addEventListener('pageshow', e => {
+ window.pageshowEvent = e;
+ });
+ }, [url]);
+ // Navigates away to trigger request sending.
+ const rc2 = await rc1.navigateToNew();
+ // Navigates back.
+ await rc2.historyBack();
+ // Verifies the page was BFCached.
+ assert_true(await rc1.executeScript(() => {
+ return window.pageshowEvent.persisted;
+ }));
+
+ // NOTE: In this case, it does not matter if BackgroundSync is on or off.
+ await expectBeacon(uuid, {count: 1});
+}, `Call fetchLater() when BFCached with activateAfter=0 sends immediately.`);
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // Sets no option to test the default behavior when a document gets discarded
+ // on navigated away.
+ const helper = new RemoteContextHelper();
+ // Opens a window without BFCache.
+ const rc1 = await helper.addWindow();
+
+ // Creates a fetchLater request in remote which should only be sent on
+ // navigating away.
+ await rc1.executeScript(url => {
+ fetchLater(url);
+ // Add a pageshow listener to stash the BFCache event.
+ window.addEventListener('pageshow', e => {
+ window.pageshowEvent = e;
+ });
+ }, [url]);
+ // Navigates away to trigger request sending.
+ const rc2 = await rc1.navigateToNew();
+ // Navigates back.
+ await rc2.historyBack();
+ // Verifies the page was NOT BFCached.
+ assert_equals(undefined, await rc1.executeScript(() => {
+ return window.pageshowEvent;
+ }));
+
+ // NOTE: In this case, it does not matter if BackgroundSync is on or off.
+ await expectBeacon(uuid, {count: 1});
+}, `fetchLater() sends on navigating away a page w/o BFCache.`);
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ // Sets no option to test the default behavior when a document gets discarded
+ // on navigated away.
+ const helper = new RemoteContextHelper();
+ // Opens a window without BFCache.
+ const rc1 = await helper.addWindow();
+
+ // Creates 2 fetchLater requests in remote, and one of them is aborted
+ // immediately. The other one should only be sent right on navigating away.
+ await rc1.executeScript(url => {
+ const controller = new AbortController();
+ fetchLater(url, {signal: controller.signal});
+ fetchLater(url);
+ controller.abort();
+ // Add a pageshow listener to stash the BFCache event.
+ window.addEventListener('pageshow', e => {
+ window.pageshowEvent = e;
+ });
+ }, [url]);
+ // Navigates away to trigger request sending.
+ const rc2 = await rc1.navigateToNew();
+ // Navigates back.
+ await rc2.historyBack();
+ // Verifies the page was NOT BFCached.
+ assert_equals(undefined, await rc1.executeScript(() => {
+ return window.pageshowEvent;
+ }));
+
+ // NOTE: In this case, it does not matter if BackgroundSync is on or off.
+ await expectBeacon(uuid, {count: 1});
+}, `fetchLater() does not send aborted request on navigating away a page w/o BFCache.`);
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const options = {activateAfter: 60000};
+ const helper = new RemoteContextHelper();
+ // Opens a window with noopener so that BFCache will work.
+ const rc1 = await helper.addWindow(
+ /*config=*/ null, /*options=*/ {features: 'noopener'});
+
+ // Creates a fetchLater request in remote which should only be sent on
+ // navigating away.
+ await rc1.executeScript((url) => {
+ // Sets activateAfter = 1m to indicate the request should NOT be sent out
+ // immediately.
+ fetchLater(url, {activateAfter: 60000});
+ // Adds a pageshow listener to stash the BFCache event.
+ window.addEventListener('pageshow', e => {
+ window.pageshowEvent = e;
+ });
+ }, [url]);
+ // Navigates away to trigger request sending.
+ const rc2 = await rc1.navigateToNew();
+ // Navigates back.
+ await rc2.historyBack();
+ // Verifies the page was BFCached.
+ assert_true(await rc1.executeScript(() => {
+ return window.pageshowEvent.persisted;
+ }));
+
+ // Theoretically, the request should still be pending thus 0 request received.
+ // However, 1 request is sent, as by default the WPT test runner, e.g.
+ // content_shell in Chromium, does not enable BackgroundSync permission,
+ // resulting in forcing request sending on every navigation, even if page is
+ // put into BFCache.
+ await expectBeacon(uuid, {count: 1});
+}, `fetchLater() with activateAfter=1m sends on page entering BFCache if BackgroundSync is off.`);
diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js
new file mode 100644
index 0000000000..ff8d9520e0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/not-send-after-abort.tentative.https.window.js
@@ -0,0 +1,23 @@
+// META: script=/common/utils.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+
+'use strict';
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+
+ // Loads an iframe that creates 2 fetchLater requests. One of them is aborted.
+ const iframe = await loadScriptAsIframe(`
+ const url = '${url}';
+ const controller = new AbortController();
+ fetchLater(url, {signal: controller.signal});
+ fetchLater(url, {method: 'POST'});
+ controller.abort();
+ `);
+ // Delete the iframe to trigger deferred request sending.
+ document.body.removeChild(iframe);
+
+ // The iframe should not send the aborted request.
+ await expectBeacon(uuid, {count: 1});
+}, 'A discarded document does not send an already aborted fetchLater request.');
diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js
new file mode 100644
index 0000000000..11e85b31a7
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple-with-activate-after.tentative.https.window.js
@@ -0,0 +1,30 @@
+// META: script=/common/utils.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+// META: timeout=long
+
+'use strict';
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const numPerMethod = 20;
+ const total = numPerMethod * 2;
+
+ // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests.
+ const iframe = await loadScriptAsIframe(`
+ const url = '${url}';
+ for (let i = 0; i < ${numPerMethod}; i++) {
+ // Changing the URL of each request to avoid HTTP Cache issue.
+ // See crbug.com/1498203#c17.
+ fetchLater(url + "&method=GET&i=" + i,
+ {method: 'GET', activateAfter: 10000}); // 10s
+ fetchLater(url + "&method=POST&i=" + i,
+ {method: 'POST', activateAfter: 8000}); // 8s
+ }
+ `);
+ // Delete the iframe to trigger deferred request sending.
+ document.body.removeChild(iframe);
+
+ // The iframe should have sent all requests.
+ await expectBeacon(uuid, {count: total});
+}, 'A discarded document sends all its fetchLater requests, no matter how much their activateAfter timeout remain.');
diff --git a/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js
new file mode 100644
index 0000000000..df34ec9ac0
--- /dev/null
+++ b/testing/web-platform/tests/fetch/fetch-later/send-on-discard/send-multiple.tentative.https.window.js
@@ -0,0 +1,28 @@
+// META: script=/common/utils.js
+// META: script=/pending-beacon/resources/pending_beacon-helper.js
+// META: timeout=long
+
+'use strict';
+
+parallelPromiseTest(async t => {
+ const uuid = token();
+ const url = generateSetBeaconURL(uuid);
+ const numPerMethod = 20;
+ const total = numPerMethod * 2;
+
+ // Loads an iframe that creates `numPerMethod` GET & POST fetchLater requests.
+ const iframe = await loadScriptAsIframe(`
+ const url = '${url}';
+ for (let i = 0; i < ${numPerMethod}; i++) {
+ // Changing the URL of each request to avoid HTTP Cache issue.
+ // See crbug.com/1498203#c17.
+ fetchLater(url + "&method=GET&i=" + i);
+ fetchLater(url + "&method=POST&i=" + i, {method: 'POST'});
+ }
+ `);
+ // Delete the iframe to trigger deferred request sending.
+ document.body.removeChild(iframe);
+
+ // The iframe should have sent all requests.
+ await expectBeacon(uuid, {count: total});
+}, 'A discarded document sends all its fetchLater requests.');