Reorganize and augment beacon wpt
- Run tests on secure contexts. I'm planning to use the sec-fetch-mode
header once WebKit supports it, which is only available on secure
contexts.
- Replace html files with window.js files.
- Merge many beacon-basic-* files into beacon-basic.https.window.js.
This is doable because the payload size limit is per context, not
per page.
- Merge beacon-readablestream.js and beacon-error.sub.window.js into
beacon-basic.https.window.js.
- Merge beacon-preflight-failure.sub.window.js into
beacon-cors.https.window.js.
- Add more cross origin tests in beacon-cors.https.window.js.
- Slim down beacon-common.sub.js.
Also remove some test files in http/tests/sendbeacon, because they are
covered by wpt/beacon.
Bug: 720303
Change-Id: Id65c70d290753428d5d17bc8e9e783969ff8bc16
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2677465
Commit-Queue: Yutaka Hirano <yhirano@chromium.org>
Reviewed-by: Dominic Farolino <dom@chromium.org>
Cr-Commit-Position: refs/heads/master@{#852515}
diff --git a/beacon/beacon-basic-blob.html b/beacon/beacon-basic-blob.html
deleted file mode 100644
index 805ea63..0000000
--- a/beacon/beacon-basic-blob.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic Blob Test</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(blobTests);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-blobMax.html b/beacon/beacon-basic-blobMax.html
deleted file mode 100644
index e18b163..0000000
--- a/beacon/beacon-basic-blobMax.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic Blob Test - MaxSize</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(blobMaxTest);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-buffersource.html b/beacon/beacon-basic-buffersource.html
deleted file mode 100644
index 14c7a4f..0000000
--- a/beacon/beacon-basic-buffersource.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic BufferSource Test</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(bufferSourceTests);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-buffersourceMax.html b/beacon/beacon-basic-buffersourceMax.html
deleted file mode 100644
index 5163ecd..0000000
--- a/beacon/beacon-basic-buffersourceMax.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic BufferSource Test - MaxSize</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(bufferSourceMaxTest);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-formdata.html b/beacon/beacon-basic-formdata.html
deleted file mode 100644
index 9f46cf6..0000000
--- a/beacon/beacon-basic-formdata.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic FormData Test</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(formDataTests);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-formdataMax.html b/beacon/beacon-basic-formdataMax.html
deleted file mode 100644
index 2c7e0fa..0000000
--- a/beacon/beacon-basic-formdataMax.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic FormData Test</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(formDataMaxTest);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-string.html b/beacon/beacon-basic-string.html
deleted file mode 100644
index cba65da..0000000
--- a/beacon/beacon-basic-string.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic String Test</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(stringTests);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic-stringMax.html b/beacon/beacon-basic-stringMax.html
deleted file mode 100644
index 005e9f5..0000000
--- a/beacon/beacon-basic-stringMax.html
+++ /dev/null
@@ -1,18 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Basic String Test - MaxSize</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
- runTests(stringMaxTest);
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-basic.https.window.js b/beacon/beacon-basic.https.window.js
new file mode 100644
index 0000000..4711771
--- /dev/null
+++ b/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/beacon/beacon-common.sub.js b/beacon/beacon-common.sub.js
index 279ad22..4699495 100644
--- a/beacon/beacon-common.sub.js
+++ b/beacon/beacon-common.sub.js
@@ -1,172 +1,111 @@
-"use strict";
+'use strict';
-// Different sizes of payloads to test.
-var smallPayloadSize = 10;
-var mediumPayloadSize = 10000;
-var largePayloadSize = 50000;
-var maxPayloadSize = 65536; // The maximum payload size allowed for a beacon request.
+const EMPTY = 'empty';
+const SMALL = 'small';
+const LARGE = 'large';
+const MAX = 'max';
+const TOOLARGE = 'toolarge';
-// String payloads of various sizes sent by sendbeacon. The format of the payloads is a string:
-// <numberOfCharacters>:<numberOfCharacters *'s>
-// ex. "10:**********"
-var smallPayload = smallPayloadSize + ":" + Array(smallPayloadSize).fill('*').join("");
-var mediumPayload = mediumPayloadSize + ":" + Array(mediumPayloadSize).fill('*').join("");
-var largePayload = largePayloadSize + ":" + Array(largePayloadSize).fill('*').join("");
-// Subtract 6 from maxPayloadSize because 65536 is 5 digits, plus 1 more for the ':'
-var maxPayload = (maxPayloadSize - 6) + ":" + Array(maxPayloadSize - 6).fill('*').join("")
+const STRING = 'string';
+const ARRAYBUFFER = 'arraybuffer';
+const FORM = 'form';
+const BLOB = 'blob';
-// Test case definitions.
-// name: String containing the unique name of the test case.
-// data: Payload object to send through sendbeacon.
-var noDataTest = { name: "NoData" };
-var nullDataTest = { name: "NullData", data: null };
-var undefinedDataTest = { name: "UndefinedData", data: undefined };
-var smallStringTest = { name: "SmallString", data: smallPayload };
-var mediumStringTest = { name: "MediumString", data: mediumPayload };
-var largeStringTest = { name: "LargeString", data: largePayload };
-var maxStringTest = { name: "MaxString", data: maxPayload };
-var emptyBlobTest = { name: "EmptyBlob", data: new Blob() };
-var smallBlobTest = { name: "SmallBlob", data: new Blob([smallPayload]) };
-var mediumBlobTest = { name: "MediumBlob", data: new Blob([mediumPayload]) };
-var largeBlobTest = { name: "LargeBlob", data: new Blob([largePayload]) };
-var maxBlobTest = { name: "MaxBlob", data: new Blob([maxPayload]) };
-var emptyBufferSourceTest = { name: "EmptyBufferSource", data: new Uint8Array() };
-var smallBufferSourceTest = { name: "SmallBufferSource", data: CreateArrayBufferFromPayload(smallPayload) };
-var mediumBufferSourceTest = { name: "MediumBufferSource", data: CreateArrayBufferFromPayload(mediumPayload) };
-var largeBufferSourceTest = { name: "LargeBufferSource", data: CreateArrayBufferFromPayload(largePayload) };
-var maxBufferSourceTest = { name: "MaxBufferSource", data: CreateArrayBufferFromPayload(maxPayload) };
-var emptyFormDataTest = { name: "EmptyFormData", data: CreateEmptyFormDataPayload() };
-var smallFormDataTest = { name: "SmallFormData", data: CreateFormDataFromPayload(smallPayload) };
-var mediumFormDataTest = { name: "MediumFormData", data: CreateFormDataFromPayload(mediumPayload) };
-var largeFormDataTest = { name: "LargeFormData", data: CreateFormDataFromPayload(largePayload) };
-var smallSafeContentTypeEncodedTest = { name: "SmallSafeContentTypeEncoded", data: new Blob([smallPayload], { type: 'application/x-www-form-urlencoded' }) };
-var smallSafeContentTypeFormTest = { name: "SmallSafeContentTypeForm", data: new FormData() };
-var smallSafeContentTypeTextTest = { name: "SmallSafeContentTypeText", data: new Blob([smallPayload], { type: 'text/plain' }) };
-var smallCORSContentTypeTextTest = { name: "SmallCORSContentTypeText", data: new Blob([smallPayload], { type: 'text/html' }) };
-// We don't test maxFormData because the extra multipart separators make it difficult to
-// calculate a maxPayload.
-
-// Test case suites.
-// Due to quota limits we split the max payload tests into their own bucket.
-var stringTests = [noDataTest, nullDataTest, undefinedDataTest, smallStringTest, mediumStringTest, largeStringTest];
-var stringMaxTest = [maxStringTest];
-var blobTests = [emptyBlobTest, smallBlobTest, mediumBlobTest, largeBlobTest];
-var blobMaxTest = [maxBlobTest];
-var bufferSourceTests = [emptyBufferSourceTest, smallBufferSourceTest, mediumBufferSourceTest, largeBufferSourceTest];
-var bufferSourceMaxTest = [maxBufferSourceTest];
-var formDataTests = [emptyFormDataTest, smallFormDataTest, mediumFormDataTest, largeFormDataTest];
-var formDataMaxTest = [largeFormDataTest];
-var contentTypeTests = [smallSafeContentTypeEncodedTest,smallSafeContentTypeFormTest,smallSafeContentTypeTextTest,smallCORSContentTypeTextTest];
-var allTests = [].concat(stringTests, stringMaxTest, blobTests, blobMaxTest, bufferSourceTests, bufferSourceMaxTest, formDataTests, formDataMaxTest, contentTypeTests);
-
-// This special cross section of test cases is meant to provide a slimmer but reasonably-
-// representative set of tests for parameterization across variables (e.g. redirect codes,
-// cors modes, etc.)
-var sampleTests = [noDataTest, nullDataTest, undefinedDataTest, smallStringTest, smallBlobTest, smallBufferSourceTest, smallFormDataTest, smallSafeContentTypeEncodedTest, smallSafeContentTypeFormTest, smallSafeContentTypeTextTest];
-
-var preflightTests = [smallCORSContentTypeTextTest];
-
-// Helper function to create an ArrayBuffer representation of a string.
-function CreateArrayBufferFromPayload(payload) {
- var length = payload.length;
- var buffer = new Uint8Array(length);
-
- for (var i = 0; i < length; i++) {
- buffer[i] = payload.charCodeAt(i);
- }
-
- return buffer;
+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}`);
+ }
}
-// Helper function to create an empty FormData object.
-function CreateEmptyFormDataPayload() {
- if (self.document === undefined) {
- return null;
- }
+// 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');
+ }
- return new FormData();
+ 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');
+ }
}
-// Helper function to create a FormData representation of a string.
-function CreateFormDataFromPayload(payload) {
- if (self.document === undefined) {
- return null;
- }
-
- var formData = new FormData();
- formData.append("payload", payload);
- return formData;
-}
-
-// Schedules promise_test's for each of the test cases.
-// Parameters:
-// testCases: An array of test cases.
-// suffix [optional]: A string used for the suffix for each test case name.
-// buildUrl [optional]: A function that returns a beacon URL given an id.
-// sendData [optional]: A function that sends the beacon with given a URL and payload.
-function runTests(testCases, suffix = '', buildUrl = self.buildUrl, sendData = self.sendData) {
- for (const testCase of testCases) {
- const id = token();
- promise_test((test) => {
- const url = buildUrl(id);
- assert_true(sendData(url, testCase.data), 'sendBeacon should succeed');
- return waitForResult(id);
- }, `Verify 'navigator.sendbeacon()' successfully sends for variant: ${testCase.name}${suffix}`);
- };
-}
-
-function buildUrl(id) {
- const baseUrl = "http://{{host}}:{{ports[http][0]}}";
- return `${baseUrl}/beacon/resources/beacon.py?cmd=store&id=${id}`;
-}
-
-// Sends the beacon for a single test. This step is factored into its own function so that
-// it can be called from a web worker. It does not check for results.
-// Note: do not assert from this method, as when called from a worker, we won't have the
-// full testharness.js test context. Instead return 'false', and the main scope will fail
-// the test.
-// Returns the result of the 'sendbeacon()' function call, true or false.
-function sendData(url, payload) {
- return self.navigator.sendBeacon(url, payload);
+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) {
- const url = `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);
+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}'`);;
- // null JSON values parse as null, not undefined
- assert_equals(results[0].error, null, "'sendbeacon' data must not fail validation");
- return;
+ if (results.length === 0) {
+ await new Promise(resolve => step_timeout(resolve, 100));
+ continue;
}
- assert_true(false, 'timeout');
-}
-
-// Creates an iframe on the document's body and runs the sample tests from the iframe.
-// The iframe is navigated immediately after it sends the data, and the window verifies
-// that the data is still successfully sent.
-function runSendInIframeAndNavigateTests() {
- var iframe = document.createElement("iframe");
- iframe.id = "iframe";
- iframe.onload = function() {
- // Clear our onload handler to prevent re-running the tests as we navigate away.
- iframe.onload = null;
- function sendData(url, payload) {
- return iframe.contentWindow.navigator.sendBeacon(url, payload);
- }
- runTests(sampleTests, '-NAVIGATE', self.buildUrl, sendData);
- // Now navigate ourselves.
- iframe.contentWindow.location = "http://{{host}}:{{ports[http][0]}}/";
- };
-
- iframe.srcdoc = '<html></html>';
- document.body.appendChild(iframe);
+ 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/beacon/beacon-cors.https.window.js b/beacon/beacon-cors.https.window.js
new file mode 100644
index 0000000..6f282a2
--- /dev/null
+++ b/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/beacon/beacon-cors.sub.window.js b/beacon/beacon-cors.sub.window.js
deleted file mode 100644
index 5543bdd..0000000
--- a/beacon/beacon-cors.sub.window.js
+++ /dev/null
@@ -1,42 +0,0 @@
-// META: timeout=long
-// META: script=/common/utils.js
-// META: script=beacon-common.sub.js
-
-"use strict";
-
-// Execute each sample test with a cross-origin URL. If allowCors is 'true'
-// the beacon handler will return CORS headers. This test ensures that the
-// sendBeacon() succeeds in either case.
-[true, false].forEach(function(allowCors) {
- function buildUrl(id) {
- const baseUrl = "http://{{domains[www]}}:{{ports[http][0]}}";
- // Note that 'allowCors=true' is not necessary for the sendBeacon() to reach
- // the server. Beacons use the HTTP POST method, which is a CORS-safelisted
- // method, and thus they do not trigger preflight. If the server does not
- // respond with Access-Control-Allow-Origin and Access-Control-Allow-Credentials
- // headers, an error will be printed to the console, but the request will
- // already have reached the server. Since beacons are fire-and-forget, the
- // error will not affect any client script, either -- not even the return
- // value of the sendBeacon() call, because the underlying fetch is asynchronous.
- // The "Beacon CORS" tests are merely testing that sendBeacon() to a cross-
- // origin URL *will* work regardless.
- const additionalQuery = allowCors ? "&origin=http://{{host}}:{{ports[http][0]}}&credentials=true" : "";
- return `${baseUrl}/beacon/resources/beacon.py?cmd=store&id=${id}${additionalQuery}`
- }
- runTests(sampleTests, allowCors ? "-CORS-ALLOW" : "-CORS-FORBID", buildUrl);
-});
-
-// Now test a cross-origin request that doesn't use a safelisted Content-Type and ensure
-// we are applying the proper restrictions. Since a non-safelisted Content-Type request
-// header is used there should be a preflight/options request and we should only succeed
-// send the payload if the proper CORS headers are used.
-{
- function buildUrl(id) {
- const baseUrl = "http://{{domains[www]}}:{{ports[http][0]}}";
- const additionalQuery = "&origin=http://{{host}}:{{ports[http][0]}}&credentials=true&preflightExpected=true";
- return `${baseUrl}/beacon/resources/beacon.py?cmd=store&id=${id}${additionalQuery}`
- }
- runTests(preflightTests, "-PREFLIGHT-ALLOW", buildUrl);
-}
-
-done();
diff --git a/beacon/beacon-error.sub.window.js b/beacon/beacon-error.sub.window.js
deleted file mode 100644
index 499fa3b..0000000
--- a/beacon/beacon-error.sub.window.js
+++ /dev/null
@@ -1,48 +0,0 @@
-// META: script=/common/utils.js
-// META: script=beacon-common.sub.js
-
-"use strict";
-
-test(function() {
- // Payload that should cause sendBeacon to return false because it exceeds the maximum payload size.
- var exceedPayload = Array(maxPayloadSize + 1).fill('z').join("");
-
- var success = navigator.sendBeacon("http://{{hosts[][nonexistent]}}", exceedPayload);
- assert_false(success, "calling 'navigator.sendBeacon()' with payload size exceeding the maximum size must fail");
-}, "Verify calling 'navigator.sendBeacon()' with a large payload returns 'false'.");
-
-test(function() {
- var invalidUrl = "http://invalid:url";
- assert_throws_js(TypeError, function() { navigator.sendBeacon(invalidUrl, smallPayload); },
- `calling 'navigator.sendBeacon()' with an invalid URL '${invalidUrl}' must throw a TypeError`);
-}, "Verify calling 'navigator.sendBeacon()' with an invalid URL throws an exception.");
-
-test(function() {
- var invalidUrl = "nothttp://invalid.url";
- assert_throws_js(TypeError, function() { navigator.sendBeacon(invalidUrl, smallPayload); },
- `calling 'navigator.sendBeacon()' with a non-http(s) URL '${invalidUrl}' must throw a TypeError`);
-}, "Verify calling 'navigator.sendBeacon()' with a URL that is not a http(s) scheme throws an exception.");
-
-// We'll validate that we can send one beacon that uses our entire Quota and then fail to send one that is just one char.
-promise_test(async () => {
- function wait(ms) {
- return new Promise(res => step_timeout(res, ms));
- }
- const url = '/fetch/api/resources/trickle.py?count=1&ms=0';
- assert_true(navigator.sendBeacon(url, maxPayload),
- "calling 'navigator.sendBeacon()' with our max payload size should succeed.");
-
- // Now we'll send just one character.
- assert_false(navigator.sendBeacon(url, '1'),
- "calling 'navigator.sendBeacon()' with just one char should fail while our Quota is used up.");
-
- for (let i = 0; i < 20; ++i) {
- await wait(100);
- if (navigator.sendBeacon(url, maxPayload)) {
- return;
- }
- }
- assert_unreached('The quota should recover after fetching.');
-}, "Verify the behavior after the quota is exhausted.");
-
-done();
diff --git a/beacon/beacon-navigate.html b/beacon/beacon-navigate.html
deleted file mode 100644
index 5df1390..0000000
--- a/beacon/beacon-navigate.html
+++ /dev/null
@@ -1,19 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>W3C Beacon Outliving Navigation Test</title>
- <meta name="timeout" content="long">
- <meta name="author" title="Microsoft Edge" href="https://www.microsoft.com">
- <script src="/resources/testharness.js"></script>
- <script src="/resources/testharnessreport.js"></script>
-</head>
-<body>
- <script src="/common/utils.js"></script>
- <script src="beacon-common.sub.js"></script>
- <script>
- "use strict";
-
- runSendInIframeAndNavigateTests("beacon");
- </script>
-</body>
-</html>
diff --git a/beacon/beacon-navigate.https.window.js b/beacon/beacon-navigate.https.window.js
new file mode 100644
index 0000000..8b42a47
--- /dev/null
+++ b/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/beacon/beacon-preflight-failure.sub.window.js b/beacon/beacon-preflight-failure.sub.window.js
deleted file mode 100644
index c5a2d81..0000000
--- a/beacon/beacon-preflight-failure.sub.window.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// META: script=/common/utils.js
-// META: script=/common/get-host-info.sub.js
-
-promise_test(async (test) => {
- const origin = get_host_info().REMOTE_ORIGIN;
- const id = token();
- const store = `${origin}/beacon/resources/beacon.py?cmd=store&id=${id}`;
- const monitor = `/beacon/resources/beacon.py?cmd=stat&id=${id}`;
-
- assert_true(navigator.sendBeacon(store, new Blob([], {type: 'x/y'})));
-
- let actual;
- for (let i = 0; i < 30; ++i) {
- await new Promise(resolve => test.step_timeout(resolve, 10));
-
- const response = await fetch(monitor);
- const obj = await response.json();
- if (obj.length > 0) {
- actual = JSON.stringify(obj);
- break;
- }
- }
-
- const expected =
- JSON.stringify([{error: 'Preflight not expected.'}]);
-
- assert_equals(actual, expected);
-});
diff --git a/beacon/beacon-readablestream.window.js b/beacon/beacon-readablestream.window.js
deleted file mode 100644
index 46e30fc..0000000
--- a/beacon/beacon-readablestream.window.js
+++ /dev/null
@@ -1,3 +0,0 @@
-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/beacon/beacon-redirect.https.window.js b/beacon/beacon-redirect.https.window.js
new file mode 100644
index 0000000..16a2545
--- /dev/null
+++ b/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/beacon/beacon-redirect.sub.window.js b/beacon/beacon-redirect.sub.window.js
deleted file mode 100644
index fd23a45..0000000
--- a/beacon/beacon-redirect.sub.window.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// META: timeout=long
-// META: script=/common/utils.js
-// META: script=beacon-common.sub.js
-
-"use strict";
-
-// 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.
-[307, 308].forEach(function(status) {
- function buildUrl(id) {
- const baseUrl = "http://{{host}}:{{ports[http][0]}}";
- const targetUrl = `${baseUrl}/beacon/resources/beacon.py?cmd=store&id=${id}`;
-
- return `/common/redirect.py?status=${status}&location=${encodeURIComponent(targetUrl)}`;
- }
- runTests(sampleTests, `-${status}`, buildUrl);
-});
-
-done();
diff --git a/beacon/resources/beacon.py b/beacon/resources/beacon.py
index f5caaf4..d81bfb1 100644
--- a/beacon/resources/beacon.py
+++ b/beacon/resources/beacon.py
@@ -24,8 +24,13 @@
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}]
+ - [{error: null, type: "text/plain;charset=UTF8"}]
- [{error: "some validation details"}]
- []
@@ -53,8 +58,9 @@
# requests we may get.
if request.method == u"POST":
payload = b""
- if b"Content-Type" in request.headers and \
- b"form-data" in request.headers[b"Content-Type"]:
+ 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")
@@ -71,20 +77,26 @@
# Confirm the payload size sent matches with the number of
# characters sent.
- if payload_size != len(payload_parts[1]):
+ if payload_size != len(payload):
error = u"expected %d characters but got %d" % (
- payload_size, len(payload_parts[1]))
+ payload_size, len(payload))
else:
# Confirm the payload contains the correct characters.
- for i in range(0, payload_size):
- if payload_parts[1][i:i+1] != b"*":
+ 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(payload_parts[1][i:i+1]))
+ 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})
+ 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