summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/beacon
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/web-platform/tests/beacon/META.yml3
-rw-r--r--testing/web-platform/tests/beacon/beacon-basic.https.window.js98
-rw-r--r--testing/web-platform/tests/beacon/beacon-common.sub.js111
-rw-r--r--testing/web-platform/tests/beacon/beacon-cors.https.window.js132
-rw-r--r--testing/web-platform/tests/beacon/beacon-navigate.https.window.js23
-rw-r--r--testing/web-platform/tests/beacon/beacon-redirect.https.window.js35
-rw-r--r--testing/web-platform/tests/beacon/headers/header-content-type-and-body.html89
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-no-referrer-when-downgrade.https.html21
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-no-referrer.html19
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-origin-when-cross-origin.html21
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-origin.html19
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-same-origin.html21
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html21
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-strict-origin.https.html21
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer-unsafe-url.https.html21
-rw-r--r--testing/web-platform/tests/beacon/headers/header-referrer.js44
-rw-r--r--testing/web-platform/tests/beacon/idlharness.any.js14
-rw-r--r--testing/web-platform/tests/beacon/resources/beacon.py118
-rw-r--r--testing/web-platform/tests/beacon/resources/content-type-and-body.py14
-rw-r--r--testing/web-platform/tests/beacon/resources/inspect-header.py18
20 files changed, 863 insertions, 0 deletions
diff --git a/testing/web-platform/tests/beacon/META.yml b/testing/web-platform/tests/beacon/META.yml
new file mode 100644
index 0000000000..4bd94e3206
--- /dev/null
+++ b/testing/web-platform/tests/beacon/META.yml
@@ -0,0 +1,3 @@
+spec: https://w3c.github.io/beacon/
+suggested_reviewers:
+ - igrigorik
diff --git a/testing/web-platform/tests/beacon/beacon-basic.https.window.js b/testing/web-platform/tests/beacon/beacon-basic.https.window.js
new file mode 100644
index 0000000000..47117716a2
--- /dev/null
+++ b/testing/web-platform/tests/beacon/beacon-basic.https.window.js
@@ -0,0 +1,98 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=beacon-common.sub.js
+
+'use strict';
+
+// TODO(yhirano): Check the sec-fetch-mode request header once WebKit supports
+// the feature.
+
+parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const id = token();
+ const url = `/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url));
+ iframe.remove();
+
+ const result = await waitForResult(id);
+ assert_equals(result.type, '(missing)', 'content-type');
+}, `simple case: with no payload`);
+
+parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const id = token();
+ const url = `/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, null));
+ iframe.remove();
+
+ const result = await waitForResult(id);
+ assert_equals(result.type, '(missing)', 'content-type');
+}, `simple case: with null payload`);
+
+for (const size of [EMPTY, SMALL, LARGE, MAX]) {
+ for (const type of [STRING, ARRAYBUFFER, FORM, BLOB]) {
+ if (size === MAX && type === FORM) {
+ // It is difficult to estimate the size of a form accurately, so we cannot
+ // test this case.
+ continue;
+ }
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(size, type);
+ const id = token();
+ const url = `/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ const result = await waitForResult(id);
+ if (getContentType(type) === null) {
+ assert_equals(result.type, '(missing)', 'content-type');
+ } else {
+ assert_true(result.type.includes(getContentType(type)), 'content-type');
+ }
+ }, `simple case: type = ${type} and size = ${size}`);
+ }
+}
+
+for (const type of [STRING, ARRAYBUFFER, FORM, BLOB]) {
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(TOOLARGE, type);
+ const id = token();
+ const url = `/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ assert_false(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ }, `Too large payload should be rejected: type = ${type}`);
+}
+
+for (const type of [STRING, ARRAYBUFFER, BLOB]) {
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ assert_true(iframe.contentWindow.navigator.sendBeacon(
+ `/beacon/resources/beacon.py?cmd=store&id=${token()}`,
+ makePayload(MAX, type)));
+ assert_true(iframe.contentWindow.navigator.sendBeacon(
+ `/beacon/resources/beacon.py?cmd=store&id=${token()}`, ''));
+ assert_false(iframe.contentWindow.navigator.sendBeacon(
+ `/beacon/resources/beacon.py?cmd=store&id=${token()}`, 'x'));
+ }, `Payload size restriction should be accumulated: type = ${type}`);
+}
+
+test(() => {
+ assert_throws_js(
+ TypeError, () => navigator.sendBeacon('...', new ReadableStream()));
+}, 'sendBeacon() with a stream does not work due to the keepalive flag being set');
diff --git a/testing/web-platform/tests/beacon/beacon-common.sub.js b/testing/web-platform/tests/beacon/beacon-common.sub.js
new file mode 100644
index 0000000000..4699495460
--- /dev/null
+++ b/testing/web-platform/tests/beacon/beacon-common.sub.js
@@ -0,0 +1,111 @@
+'use strict';
+
+const EMPTY = 'empty';
+const SMALL = 'small';
+const LARGE = 'large';
+const MAX = 'max';
+const TOOLARGE = 'toolarge';
+
+const STRING = 'string';
+const ARRAYBUFFER = 'arraybuffer';
+const FORM = 'form';
+const BLOB = 'blob';
+
+function getContentType(type) {
+ switch (type) {
+ case STRING:
+ return 'text/plain;charset=UTF-8';
+ case ARRAYBUFFER:
+ return null;
+ case FORM:
+ return 'multipart/form-data';
+ case BLOB:
+ return null;
+ default:
+ throw Error(`invalid type: ${type}`);
+ }
+}
+
+// Create a payload with the given size and type.
+// `sizeString` must be one of EMPTY, SMALL, LARGE, MAX, TOOLARGE.
+// `type` must be one of STRING, ARRAYBUFFER, FORM, BLOB.
+// `contentType` is effective only if `type` is BLOB.
+function makePayload(sizeString, type, contentType) {
+ let size = 0;
+ switch (sizeString) {
+ case EMPTY:
+ size = 0;
+ break;
+ case SMALL:
+ size = 10;
+ break;
+ case LARGE:
+ size = 10 * 1000;
+ break;
+ case MAX:
+ if (type === FORM) {
+ throw Error('Not supported');
+ }
+ size = 65536;
+ break;
+ case TOOLARGE:
+ size = 65537;
+ break;
+ default:
+ throw Error('invalid size');
+ }
+
+ let data = '';
+ if (size > 0) {
+ const prefix = String(size) + ':';
+ data = prefix + Array(size - prefix.length).fill('*').join('');
+ }
+
+ switch (type) {
+ case STRING:
+ return data;
+ case ARRAYBUFFER:
+ return new TextEncoder().encode(data).buffer;
+ case FORM:
+ const formData = new FormData();
+ if (size > 0) {
+ formData.append('payload', data);
+ }
+ return formData;
+ case BLOB:
+ const options = contentType ? {type: contentType} : undefined;
+ const blob = new Blob([data], options);
+ return blob;
+ default:
+ throw Error('invalid type');
+ }
+}
+
+function parallelPromiseTest(func, description) {
+ async_test((t) => {
+ Promise.resolve(func(t)).then(() => t.done()).catch(t.step_func((e) => {
+ throw e;
+ }));
+ }, description);
+}
+
+// Poll the server for the test result.
+async function waitForResult(id, expectedError = null) {
+ const url = `/beacon/resources/beacon.py?cmd=stat&id=${id}`;
+ for (let i = 0; i < 30; ++i) {
+ const response = await fetch(url);
+ const text = await response.text();
+ const results = JSON.parse(text);
+
+ if (results.length === 0) {
+ await new Promise(resolve => step_timeout(resolve, 100));
+ continue;
+ }
+ assert_equals(results.length, 1, `bad response: '${text}'`);
+ const result = results[0];
+ // null JSON values parse as null, not undefined
+ assert_equals(result.error, expectedError, 'error recorded in stash');
+ return result;
+ }
+ assert_true(false, 'timeout');
+}
diff --git a/testing/web-platform/tests/beacon/beacon-cors.https.window.js b/testing/web-platform/tests/beacon/beacon-cors.https.window.js
new file mode 100644
index 0000000000..6f282a23b1
--- /dev/null
+++ b/testing/web-platform/tests/beacon/beacon-cors.https.window.js
@@ -0,0 +1,132 @@
+// META: timeout=long
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=beacon-common.sub.js
+
+'use strict';
+
+const {HTTPS_ORIGIN, ORIGIN, HTTPS_REMOTE_ORIGIN} = get_host_info();
+
+// As /common/redirect.py is not under our control, let's make sure that
+// it doesn't support CORS.
+parallelPromiseTest(async (t) => {
+ const destination = `${HTTPS_REMOTE_ORIGIN}/common/text-plain.txt` +
+ `?pipe=header(access-control-allow-origin,*)`;
+ const redirect = `${HTTPS_REMOTE_ORIGIN}/common/redirect.py` +
+ `?location=${encodeURIComponent(destination)}`;
+
+ // Fetching `destination` is fine because it supports CORS.
+ await fetch(destination);
+
+ // Fetching redirect.py should fail because it doesn't support CORS.
+ await promise_rejects_js(t, TypeError, fetch(redirect));
+}, '/common/redirect.py does not support CORS');
+
+for (const type of [STRING, ARRAYBUFFER, FORM, BLOB]) {
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, type);
+ const id = token();
+ // As we use "no-cors" for CORS-safelisted requests, the redirect is
+ // processed without an error while the request is cross-origin and the
+ // redirect handler doesn't support CORS.
+ const destination =
+ `${HTTPS_REMOTE_ORIGIN}/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ const url = `${HTTPS_REMOTE_ORIGIN}/common/redirect.py` +
+ `?status=307&location=${encodeURIComponent(destination)}`;
+
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ await waitForResult(id);
+ }, `cross-origin, CORS-safelisted: type = ${type}`);
+}
+
+parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, BLOB, 'application/octet-stream');
+ const id = token();
+ const destination =
+ `${HTTPS_REMOTE_ORIGIN}/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ const url = `${HTTPS_REMOTE_ORIGIN}/common/redirect.py` +
+ `?status=307&location=${encodeURIComponent(destination)}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ // The beacon is rejected during redirect handling because /common/redirect.py
+ // doesn't support CORS.
+
+ await new Promise((resolve) => step_timeout(resolve, 3000));
+ const res = await fetch(`/beacon/resources/beacon.py?cmd=stat&id=${id}`);
+ assert_equals((await res.json()).length, 0);
+}, `cross-origin, non-CORS-safelisted: failure case (with redirect)`);
+
+parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, BLOB, 'application/octet-stream');
+ const id = token();
+ const url =
+ `${HTTPS_REMOTE_ORIGIN}/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ // The beacon is rejected during preflight handling.
+ await waitForResult(id, /*expectedError=*/ 'Preflight not expected.');
+}, `cross-origin, non-CORS-safelisted: failure case (without redirect)`);
+
+for (const credentials of [false, true]) {
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, BLOB, 'application/octet-stream');
+ const id = token();
+ let url = `${HTTPS_REMOTE_ORIGIN}/beacon/resources/beacon.py` +
+ `?cmd=store&id=${id}&preflightExpected&origin=${ORIGIN}`;
+ if (credentials) {
+ url += `&credentials=true`;
+ }
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ // We need access-control-allow-credentials in the preflight response. This
+ // shows that the request's credentials mode is 'include'.
+ if (credentials) {
+ const result = await waitForResult(id);
+ assert_equals(result.type, 'application/octet-stream');
+ } else {
+ await new Promise((resolve) => step_timeout(resolve, 3000));
+ const res = await fetch(`/beacon/resources/beacon.py?cmd=stat&id=${id}`);
+ assert_equals((await res.json()).length, 0);
+ }
+ }, `cross-origin, non-CORS-safelisted[credentials=${credentials}]`);
+}
+
+parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, BLOB, 'application/octet-stream');
+ const id = token();
+ const destination = `${HTTPS_REMOTE_ORIGIN}/beacon/resources/beacon.py` +
+ `?cmd=store&id=${id}&preflightExpected&origin=${ORIGIN}&credentials=true`;
+ const url = `${HTTPS_REMOTE_ORIGIN}/fetch/api/resources/redirect.py` +
+ `?redirect_status=307&allow_headers=content-type` +
+ `&location=${encodeURIComponent(destination)}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ const result = await waitForResult(id);
+ assert_equals(result.type, 'application/octet-stream');
+}, `cross-origin, non-CORS-safelisted success-case (with redirect)`);
diff --git a/testing/web-platform/tests/beacon/beacon-navigate.https.window.js b/testing/web-platform/tests/beacon/beacon-navigate.https.window.js
new file mode 100644
index 0000000000..8b42a47cd9
--- /dev/null
+++ b/testing/web-platform/tests/beacon/beacon-navigate.https.window.js
@@ -0,0 +1,23 @@
+// META: timeout=long
+// META: script=/common/utils.js
+// META: script=/common/get-host-info.sub.js
+// META: script=beacon-common.sub.js
+
+'use strict';
+
+const {HTTP_REMOTE_ORIGIN} = get_host_info();
+
+for (const type of [STRING, ARRAYBUFFER, FORM, BLOB]) {
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, type);
+ const id = token();
+ const url = `/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+
+ iframe.src = `${HTTP_REMOTE_ORIGIN}/common/blank.html`;
+ }, `The frame navigates away after calling sendBeacon[type = ${type}].`);
+}
diff --git a/testing/web-platform/tests/beacon/beacon-redirect.https.window.js b/testing/web-platform/tests/beacon/beacon-redirect.https.window.js
new file mode 100644
index 0000000000..16a2545527
--- /dev/null
+++ b/testing/web-platform/tests/beacon/beacon-redirect.https.window.js
@@ -0,0 +1,35 @@
+// META: timeout=long
+// META: script=/common/get-host-info.sub.js
+// META: script=/common/utils.js
+// META: script=beacon-common.sub.js
+
+'use strict';
+
+const {ORIGIN} = get_host_info();
+
+// Execute each sample test per redirect status code.
+// Note that status codes 307 and 308 are the only codes that will maintain POST
+// data through a redirect.
+for (const status of [307, 308]) {
+ for (const type of [STRING, ARRAYBUFFER, FORM, BLOB]) {
+ parallelPromiseTest(async (t) => {
+ const iframe = document.createElement('iframe');
+ document.body.appendChild(iframe);
+ t.add_cleanup(() => iframe.remove());
+
+ const payload = makePayload(SMALL, type);
+ const id = token();
+ const destination =
+ `${ORIGIN}/beacon/resources/beacon.py?cmd=store&id=${id}`;
+ const url = `${ORIGIN}/common/redirect.py` +
+ `?status=${status}&location=${encodeURIComponent(destination)}`;
+
+ assert_true(iframe.contentWindow.navigator.sendBeacon(url, payload));
+ iframe.remove();
+
+ await waitForResult(id);
+ }, `cross-origin, CORS-safelisted: status = ${status}, type = ${type}`);
+ }
+};
+
+done();
diff --git a/testing/web-platform/tests/beacon/headers/header-content-type-and-body.html b/testing/web-platform/tests/beacon/headers/header-content-type-and-body.html
new file mode 100644
index 0000000000..0369cffdf4
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-content-type-and-body.html
@@ -0,0 +1,89 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Content-Type header</title>
+ <script src=/resources/testharness.js></script>
+ <script src=/resources/testharnessreport.js></script>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script>
+const RESOURCES_DIR = "/beacon/resources/";
+
+function testContentTypeAndBody(what, expected, title) {
+ function wait(ms) {
+ return new Promise(resolve => step_timeout(resolve, ms));
+ }
+ promise_test(async t => {
+ const id = self.token();
+ const testUrl = new Request(RESOURCES_DIR + "content-type-and-body.py?cmd=put&id=" + id).url;
+ assert_equals(performance.getEntriesByName(testUrl).length, 0);
+ assert_true(navigator.sendBeacon(testUrl, what), "SendBeacon Succeeded");
+
+ do {
+ await wait(50);
+ } while (performance.getEntriesByName(testUrl).length === 0);
+ assert_equals(performance.getEntriesByName(testUrl).length, 1);
+ const checkUrl = RESOURCES_DIR + "content-type-and-body.py?cmd=get&id=" + id;
+ const response = await fetch(checkUrl);
+ const text = await response.text();
+ if (expected.startsWith("multipart/form-data")) {
+ const split = expected.split(":");
+ const contentType = split[0];
+ const contentDisposition = "Content-Disposition: form-data; name=\"" + split[1] + "\"; filename=\"blob\"";
+ assert_true(text.startsWith(contentType), "Correct Content-Type header result");
+ assert_true(text.includes(contentDisposition), "Body included value");
+ } else {
+ assert_equals(text, expected, "Correct Content-Type header result");
+ }
+ }, "Test content-type header for a body " + title);
+}
+
+function stringToArrayBufferView(input) {
+ var buffer = new ArrayBuffer(input.length * 2);
+ var view = new Uint16Array(buffer);
+
+ // dumbly copy over the bytes
+ for (var i = 0, len = input.length; i < len; i++) {
+ view[i] = input.charCodeAt(i);
+ }
+ return view;
+}
+
+function stringToArrayBuffer(input) {
+ var buffer = new ArrayBuffer(input.length * 2);
+ var view = new Uint16Array(buffer);
+
+ // dumbly copy over the bytes
+ for (var i = 0, len = input.length; i < len; i++) {
+ view[i] = input.charCodeAt(i);
+ }
+ return buffer;
+}
+
+function stringToBlob(input) {
+ return new Blob([input], {type: "text/plain"});
+}
+
+function stringToFormData(input) {
+ var formdata = new FormData();
+ formdata.append(input, new Blob(['hi']));
+ return formdata;
+}
+
+function stringToURLSearchParams(input)
+{
+ return new URLSearchParams(input);
+}
+
+testContentTypeAndBody("hi!", "text/plain;charset=UTF-8: hi!", "string");
+testContentTypeAndBody(stringToArrayBufferView("123"), ": 1\0" + "2\0" + "3\0", "ArrayBufferView");
+testContentTypeAndBody(stringToArrayBuffer("123"), ": 1\0" + "2\0" + "3\0", "ArrayBuffer");
+testContentTypeAndBody(stringToBlob("123"), "text/plain: 123", "Blob");
+testContentTypeAndBody(stringToFormData("qwerty"), "multipart/form-data:qwerty", "FormData");
+testContentTypeAndBody(stringToURLSearchParams("key1=value1&key2=value2"), "application/x-www-form-urlencoded;charset=UTF-8: key1=value1&key2=value2", "URLSearchParams");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-no-referrer-when-downgrade.https.html b/testing/web-platform/tests/beacon/headers/header-referrer-no-referrer-when-downgrade.https.html
new file mode 100644
index 0000000000..d09d4ea560
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-no-referrer-when-downgrade.https.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header No Referrer When Downgrade Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='no-referrer-when-downgrade'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/beacon/headers/header-referrer.js"></script>
+ <script>
+ var testBase = get_host_info().HTTPS_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerUrl);
+ testBase = get_host_info().HTTP_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, "", true);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-no-referrer.html b/testing/web-platform/tests/beacon/headers/header-referrer-no-referrer.html
new file mode 100644
index 0000000000..b26db4c2d5
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-no-referrer.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header No Referrer Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='no-referrer'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="header-referrer.js"></script>
+ <script>
+ var testBase = RESOURCES_DIR;
+ testReferrerHeader(testBase, "");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-origin-when-cross-origin.html b/testing/web-platform/tests/beacon/headers/header-referrer-origin-when-cross-origin.html
new file mode 100644
index 0000000000..a23c0210c5
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-origin-when-cross-origin.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header Origin When Cross Origin Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='origin-when-cross-origin'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="header-referrer.js"></script>
+ <script>
+ var testBase = get_host_info().HTTP_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerUrl);
+ testBase = get_host_info().HTTP_REMOTE_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerOrigin);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-origin.html b/testing/web-platform/tests/beacon/headers/header-referrer-origin.html
new file mode 100644
index 0000000000..c7a571e3a1
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-origin.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header Origin Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='origin'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="header-referrer.js"></script>
+ <script>
+ var testBase = get_host_info().HTTP_REMOTE_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerOrigin);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-same-origin.html b/testing/web-platform/tests/beacon/headers/header-referrer-same-origin.html
new file mode 100644
index 0000000000..80455ab59e
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-same-origin.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header Same Origin Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='same-origin'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="header-referrer.js"></script>
+ <script>
+ var testBase = RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerUrl);
+ testBase = get_host_info().HTTP_REMOTE_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, "");
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html b/testing/web-platform/tests/beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html
new file mode 100644
index 0000000000..f310035009
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-strict-origin-when-cross-origin.https.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header Strict Origin Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='strict-origin'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/beacon/headers/header-referrer.js"></script>
+ <script>
+ var testBase = get_host_info().HTTPS_REMOTE_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerOrigin);
+ testBase = get_host_info().HTTP_REMOTE_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, "", true);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-strict-origin.https.html b/testing/web-platform/tests/beacon/headers/header-referrer-strict-origin.https.html
new file mode 100644
index 0000000000..b65bc795d2
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-strict-origin.https.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header Strict Origin Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='strict-origin'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/beacon/headers/header-referrer.js"></script>
+ <script>
+ var testBase = get_host_info().HTTPS_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerOrigin);
+ testBase = get_host_info().HTTP_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, "", true);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer-unsafe-url.https.html b/testing/web-platform/tests/beacon/headers/header-referrer-unsafe-url.https.html
new file mode 100644
index 0000000000..26a062ebd8
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer-unsafe-url.https.html
@@ -0,0 +1,21 @@
+<!doctype html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>SendBeacon Referrer Header Unsafe Url Policy</title>
+ <script src="/resources/testharness.js"></script>
+ <script src="/resources/testharnessreport.js"></script>
+ <meta name='referrer' content='unsafe-url'>
+ </head>
+ <body>
+ <script src="/common/utils.js"></script>
+ <script src="/common/get-host-info.sub.js"></script>
+ <script src="/beacon/headers/header-referrer.js"></script>
+ <script>
+ var testBase = get_host_info().HTTPS_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerUrl);
+ testBase = get_host_info().HTTP_ORIGIN + RESOURCES_DIR;
+ testReferrerHeader(testBase, referrerUrl, true);
+ </script>
+ </body>
+</html>
diff --git a/testing/web-platform/tests/beacon/headers/header-referrer.js b/testing/web-platform/tests/beacon/headers/header-referrer.js
new file mode 100644
index 0000000000..ebd67df1d7
--- /dev/null
+++ b/testing/web-platform/tests/beacon/headers/header-referrer.js
@@ -0,0 +1,44 @@
+var RESOURCES_DIR = "/beacon/resources/";
+
+var referrerOrigin = self.location.origin + '/';
+var referrerUrl = self.location.href;
+
+function testReferrerHeader(testBase, expectedReferrer, mayBeBlockedAsMixedContent = false) {
+ var id = self.token();
+ var testUrl = testBase + "inspect-header.py?header=referer&cmd=put&id=" + id;
+
+ promise_test(function(test) {
+ const sentBeacon = navigator.sendBeacon(testUrl);
+ if (mayBeBlockedAsMixedContent && !sentBeacon)
+ return Promise.resolve();
+ assert_true(sentBeacon, "SendBeacon Succeeded");
+ return pollResult(expectedReferrer, id) .then(result => {
+ assert_equals(result, expectedReferrer, "Correct referrer header result");
+ });
+ }, "Test referer header " + testBase);
+}
+
+// SendBeacon is an asynchronous and non-blocking request to a web server.
+// We may have to create a poll loop to get result from server
+function pollResult(expectedReferrer, id) {
+ var checkUrl = RESOURCES_DIR + "inspect-header.py?header=referer&cmd=get&id=" + id;
+
+ return new Promise(resolve => {
+ function checkResult() {
+ fetch(checkUrl).then(
+ function(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/beacon/idlharness.any.js b/testing/web-platform/tests/beacon/idlharness.any.js
new file mode 100644
index 0000000000..bf267ab8bd
--- /dev/null
+++ b/testing/web-platform/tests/beacon/idlharness.any.js
@@ -0,0 +1,14 @@
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+// https://w3c.github.io/beacon/
+
+idl_test(
+ ['beacon'],
+ ['html'],
+ idl_array => {
+ idl_array.add_objects({
+ Navigator: ['navigator'],
+ });
+ }
+);
diff --git a/testing/web-platform/tests/beacon/resources/beacon.py b/testing/web-platform/tests/beacon/resources/beacon.py
new file mode 100644
index 0000000000..d81bfb1ac6
--- /dev/null
+++ b/testing/web-platform/tests/beacon/resources/beacon.py
@@ -0,0 +1,118 @@
+import json
+
+from wptserve.utils import isomorphic_decode
+
+def main(request, response):
+ """Helper handler for Beacon tests.
+
+ It handles two forms of requests:
+
+ STORE:
+ A URL with a query string of the form 'cmd=store&id=<token>'.
+
+ Stores the receipt of a sendBeacon() request along with its validation
+ result, returning HTTP 200 OK.
+
+ if "preflightExpected" exists in the query, this handler responds to
+ CORS preflights.
+
+ STAT:
+ A URL with a query string of the form 'cmd=stat&id=<token>'.
+
+ Retrieves the results of test for the given id and returns them as a
+ JSON array and HTTP 200 OK status code. Due to the eventual read-once
+ nature of the stash, results for a given test are only guaranteed to be
+ returned once, though they may be returned multiple times.
+
+ An entry may contain following members.
+ - error: An error string. null if there is no error.
+ - type: The content-type header of the request "(missing)" if there
+ is no content-type header in the request.
+
+ Example response bodies:
+ - [{error: null, type: "text/plain;charset=UTF8"}]
+ - [{error: "some validation details"}]
+ - []
+
+ Common parameters:
+ cmd - the command, 'store' or 'stat'.
+ id - the unique identifier of the test.
+ """
+
+ id = request.GET.first(b"id")
+ command = request.GET.first(b"cmd").lower()
+
+ # Append CORS headers if needed.
+ if b"origin" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Origin",
+ request.GET.first(b"origin"))
+ if b"credentials" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Credentials",
+ request.GET.first(b"credentials"))
+
+ # Handle the 'store' and 'stat' commands.
+ if command == b"store":
+ error = None
+
+ # Only store the actual POST requests, not any preflight/OPTIONS
+ # requests we may get.
+ if request.method == u"POST":
+ payload = b""
+ contentType = request.headers[b"Content-Type"] \
+ if b"Content-Type" in request.headers else b"(missing)"
+ if b"form-data" in contentType:
+ if b"payload" in request.POST:
+ # The payload was sent as a FormData.
+ payload = request.POST.first(b"payload")
+ else:
+ # A FormData was sent with an empty payload.
+ pass
+ else:
+ # The payload was sent as either a string, Blob, or BufferSource.
+ payload = request.body
+
+ payload_parts = list(filter(None, payload.split(b":")))
+ if len(payload_parts) > 0:
+ payload_size = int(payload_parts[0])
+
+ # Confirm the payload size sent matches with the number of
+ # characters sent.
+ if payload_size != len(payload):
+ error = u"expected %d characters but got %d" % (
+ payload_size, len(payload))
+ else:
+ # Confirm the payload contains the correct characters.
+ for i in range(len(payload)):
+ if i <= len(payload_parts[0]):
+ continue
+ c = payload[i:i+1]
+ if c != b"*":
+ error = u"expected '*' at index %d but got '%s''" % (
+ i, isomorphic_decode(c))
+ break
+
+ # Store the result in the stash so that it can be retrieved
+ # later with a 'stat' command.
+ request.server.stash.put(id, {
+ u"error": error,
+ u"type": isomorphic_decode(contentType)
+ })
+ elif request.method == u"OPTIONS":
+ # If we expect a preflight, then add the cors headers we expect,
+ # otherwise log an error as we shouldn't send a preflight for all
+ # requests.
+ if b"preflightExpected" in request.GET:
+ response.headers.set(b"Access-Control-Allow-Headers",
+ b"content-type")
+ response.headers.set(b"Access-Control-Allow-Methods", b"POST")
+ else:
+ error = u"Preflight not expected."
+ request.server.stash.put(id, {u"error": error})
+ elif command == b"stat":
+ test_data = request.server.stash.take(id)
+ results = [test_data] if test_data else []
+
+ response.headers.set(b"Content-Type", b"text/plain")
+ response.content = json.dumps(results)
+ else:
+ response.status = 400 # BadRequest
diff --git a/testing/web-platform/tests/beacon/resources/content-type-and-body.py b/testing/web-platform/tests/beacon/resources/content-type-and-body.py
new file mode 100644
index 0000000000..9b1e880c2f
--- /dev/null
+++ b/testing/web-platform/tests/beacon/resources/content-type-and-body.py
@@ -0,0 +1,14 @@
+def main(request, response):
+ command = request.GET.first(b"cmd").lower()
+ test_id = request.GET.first(b"id")
+ if command == b"put":
+ request.server.stash.put(test_id, request.headers.get(b"Content-Type", b"") + b": " + request.body)
+ return [(b"Content-Type", b"text/plain")], u""
+
+ if command == b"get":
+ stashed_header = request.server.stash.take(test_id)
+ if stashed_header is not None:
+ return [(b"Content-Type", b"text/plain")], stashed_header
+
+ response.set_error(400, u"Bad Command")
+ return u"ERROR: Bad Command!"
diff --git a/testing/web-platform/tests/beacon/resources/inspect-header.py b/testing/web-platform/tests/beacon/resources/inspect-header.py
new file mode 100644
index 0000000000..f926ed43fc
--- /dev/null
+++ b/testing/web-platform/tests/beacon/resources/inspect-header.py
@@ -0,0 +1,18 @@
+def main(request, response):
+ headers = [(b"Content-Type", b"text/plain")]
+ command = request.GET.first(b"cmd").lower()
+ test_id = request.GET.first(b"id")
+ header = request.GET.first(b"header")
+ if command == b"put":
+ request.server.stash.put(test_id, request.headers.get(header, b""))
+
+ elif command == b"get":
+ stashed_header = request.server.stash.take(test_id)
+ if stashed_header is not None:
+ headers.append((b"x-request-" + header, stashed_header))
+
+ else:
+ response.set_error(400, u"Bad Command")
+ return u"ERROR: Bad Command!"
+
+ return headers, u""