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