From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- .../mochitest/fetch/common_temporaryFileBlob.js | 39 + dom/tests/mochitest/fetch/empty.js | 0 dom/tests/mochitest/fetch/empty.js^headers^ | 1 + dom/tests/mochitest/fetch/fetch_test_framework.js | 165 ++ dom/tests/mochitest/fetch/message_receiver.html | 6 + dom/tests/mochitest/fetch/mochitest.ini | 57 + dom/tests/mochitest/fetch/nested_worker_wrapper.js | 28 + dom/tests/mochitest/fetch/reroute.html | 18 + dom/tests/mochitest/fetch/reroute.js | 24 + dom/tests/mochitest/fetch/reroute.js^headers^ | 1 + dom/tests/mochitest/fetch/sw_reroute.js | 31 + dom/tests/mochitest/fetch/test_fetch_basic.html | 23 + dom/tests/mochitest/fetch/test_fetch_basic.js | 104 ++ .../mochitest/fetch/test_fetch_basic_http.html | 23 + dom/tests/mochitest/fetch/test_fetch_basic_http.js | 201 +++ .../test_fetch_basic_http_sw_empty_reroute.html | 22 + .../fetch/test_fetch_basic_http_sw_reroute.html | 22 + .../fetch/test_fetch_basic_sw_empty_reroute.html | 22 + .../fetch/test_fetch_basic_sw_reroute.html | 22 + dom/tests/mochitest/fetch/test_fetch_cors.html | 23 + dom/tests/mochitest/fetch/test_fetch_cors.js | 1748 ++++++++++++++++++++ .../fetch/test_fetch_cors_sw_empty_reroute.html | 22 + .../fetch/test_fetch_cors_sw_reroute.html | 22 + .../mochitest/fetch/test_formdataparsing.html | 23 + dom/tests/mochitest/fetch/test_formdataparsing.js | 283 ++++ .../fetch/test_formdataparsing_sw_reroute.html | 22 + dom/tests/mochitest/fetch/test_headers.html | 17 + dom/tests/mochitest/fetch/test_headers_common.js | 229 +++ .../mochitest/fetch/test_headers_mainthread.html | 155 ++ .../mochitest/fetch/test_headers_sw_reroute.html | 16 + dom/tests/mochitest/fetch/test_request.html | 23 + dom/tests/mochitest/fetch/test_request.js | 542 ++++++ .../mochitest/fetch/test_request_context.html | 19 + .../mochitest/fetch/test_request_sw_reroute.html | 22 + dom/tests/mochitest/fetch/test_response.html | 23 + dom/tests/mochitest/fetch/test_response.js | 267 +++ .../mochitest/fetch/test_response_sw_reroute.html | 22 + .../mochitest/fetch/test_temporaryFileBlob.html | 35 + dom/tests/mochitest/fetch/utils.js | 37 + .../mochitest/fetch/worker_temporaryFileBlob.js | 21 + dom/tests/mochitest/fetch/worker_wrapper.js | 58 + 41 files changed, 4438 insertions(+) create mode 100644 dom/tests/mochitest/fetch/common_temporaryFileBlob.js create mode 100644 dom/tests/mochitest/fetch/empty.js create mode 100644 dom/tests/mochitest/fetch/empty.js^headers^ create mode 100644 dom/tests/mochitest/fetch/fetch_test_framework.js create mode 100644 dom/tests/mochitest/fetch/message_receiver.html create mode 100644 dom/tests/mochitest/fetch/mochitest.ini create mode 100644 dom/tests/mochitest/fetch/nested_worker_wrapper.js create mode 100644 dom/tests/mochitest/fetch/reroute.html create mode 100644 dom/tests/mochitest/fetch/reroute.js create mode 100644 dom/tests/mochitest/fetch/reroute.js^headers^ create mode 100644 dom/tests/mochitest/fetch/sw_reroute.js create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic.js create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic_http.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic_http.js create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_cors.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_cors.js create mode 100644 dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_formdataparsing.html create mode 100644 dom/tests/mochitest/fetch/test_formdataparsing.js create mode 100644 dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_headers.html create mode 100644 dom/tests/mochitest/fetch/test_headers_common.js create mode 100644 dom/tests/mochitest/fetch/test_headers_mainthread.html create mode 100644 dom/tests/mochitest/fetch/test_headers_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_request.html create mode 100644 dom/tests/mochitest/fetch/test_request.js create mode 100644 dom/tests/mochitest/fetch/test_request_context.html create mode 100644 dom/tests/mochitest/fetch/test_request_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_response.html create mode 100644 dom/tests/mochitest/fetch/test_response.js create mode 100644 dom/tests/mochitest/fetch/test_response_sw_reroute.html create mode 100644 dom/tests/mochitest/fetch/test_temporaryFileBlob.html create mode 100644 dom/tests/mochitest/fetch/utils.js create mode 100644 dom/tests/mochitest/fetch/worker_temporaryFileBlob.js create mode 100644 dom/tests/mochitest/fetch/worker_wrapper.js (limited to 'dom/tests/mochitest/fetch') diff --git a/dom/tests/mochitest/fetch/common_temporaryFileBlob.js b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js new file mode 100644 index 000000000..47f5695b1 --- /dev/null +++ b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js @@ -0,0 +1,39 @@ +var data = new Array(256).join("1234567890ABCDEF"); + +function test_basic() { + info("Simple test"); + + fetch("/tests/dom/xhr/tests/temporaryFileBlob.sjs", + { method: "POST", body: data }) + .then(response => { + return response.blob(); + }).then(blob => { + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function() { + is(fr.result, data, "Data content matches"); + next(); + } + }); +} + +function test_worker() { + info("XHR in workers"); + var w = new Worker('worker_temporaryFileBlob.js'); + w.onmessage = function(e) { + if (e.data.type == 'info') { + info(e.data.msg); + } else if (e.data.type == 'check') { + ok(e.data.what, e.data.msg); + } else if (e.data.type == 'finish') { + next(); + } else { + ok(false, 'Something wrong happened'); + } + } + + w.postMessage(42); +} diff --git a/dom/tests/mochitest/fetch/empty.js b/dom/tests/mochitest/fetch/empty.js new file mode 100644 index 000000000..e69de29bb diff --git a/dom/tests/mochitest/fetch/empty.js^headers^ b/dom/tests/mochitest/fetch/empty.js^headers^ new file mode 100644 index 000000000..d0b9633bb --- /dev/null +++ b/dom/tests/mochitest/fetch/empty.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: / diff --git a/dom/tests/mochitest/fetch/fetch_test_framework.js b/dom/tests/mochitest/fetch/fetch_test_framework.js new file mode 100644 index 000000000..8ded0911f --- /dev/null +++ b/dom/tests/mochitest/fetch/fetch_test_framework.js @@ -0,0 +1,165 @@ +function testScript(script) { + + // The framework runs the entire test in many different configurations. + // On slow platforms and builds this can make the tests likely to + // timeout while they are still running. Lengthen the timeout to + // accomodate this. + SimpleTest.requestLongerTimeout(2); + + // reroute.html should have set this variable if a service worker is present! + if (!("isSWPresent" in window)) { + window.isSWPresent = false; + } + + function setupPrefs() { + return new Promise(function(resolve, reject) { + SpecialPowers.pushPrefEnv({ + "set": [["dom.requestcontext.enabled", true], + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true]] + }, resolve); + }); + } + + function workerTest() { + return new Promise(function(resolve, reject) { + var worker = new Worker("worker_wrapper.js"); + worker.onmessage = function(event) { + if (event.data.context != "Worker") { + return; + } + if (event.data.type == 'finish') { + resolve(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + } + worker.onerror = function(event) { + reject("Worker error: " + event.message); + }; + + worker.postMessage({ "script": script }); + }); + } + + function nestedWorkerTest() { + return new Promise(function(resolve, reject) { + var worker = new Worker("nested_worker_wrapper.js"); + worker.onmessage = function(event) { + if (event.data.context != "NestedWorker") { + return; + } + if (event.data.type == 'finish') { + resolve(); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + } + worker.onerror = function(event) { + reject("Nested Worker error: " + event.message); + }; + + worker.postMessage({ "script": script }); + }); + } + + function serviceWorkerTest() { + var isB2G = !navigator.userAgent.includes("Android") && + /Mobile|Tablet/.test(navigator.userAgent); + if (isB2G) { + // TODO B2G doesn't support running service workers for now due to bug 1137683. + dump("Skipping running the test in SW until bug 1137683 gets fixed.\n"); + return Promise.resolve(); + } + return new Promise(function(resolve, reject) { + function setupSW(registration) { + var worker = registration.waiting || + registration.active; + + window.addEventListener("message",function onMessage(event) { + if (event.data.context != "ServiceWorker") { + return; + } + if (event.data.type == 'finish') { + window.removeEventListener("message", onMessage); + registration.unregister() + .then(resolve) + .catch(reject); + } else if (event.data.type == 'status') { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + }, false); + + worker.onerror = reject; + + var iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function() { + worker.postMessage({ script: script }); + }; + document.body.appendChild(iframe); + } + + navigator.serviceWorker.register("worker_wrapper.js", {scope: "."}) + .then(function(registration) { + if (registration.installing) { + var done = false; + registration.installing.onstatechange = function() { + if (!done) { + done = true; + setupSW(registration); + } + }; + } else { + setupSW(registration); + } + }); + }); + } + + function windowTest() { + return new Promise(function(resolve, reject) { + var scriptEl = document.createElement("script"); + scriptEl.setAttribute("src", script); + scriptEl.onload = function() { + runTest().then(resolve, reject); + }; + document.body.appendChild(scriptEl); + }); + } + + SimpleTest.waitForExplicitFinish(); + // We have to run the window, worker and service worker tests sequentially + // since some tests set and compare cookies and running in parallel can lead + // to conflicting values. + setupPrefs() + .then(function() { + return windowTest(); + }) + .then(function() { + return workerTest(); + }) + .then(function() { + // XXX Bug 1281212 - This makes other, unrelated test suites fail, primarily on WinXP. + let isWin = navigator.platform.indexOf("Win") == 0; + return isWin ? undefined : nestedWorkerTest(); + }) + .then(function() { + return serviceWorkerTest(); + }) + .catch(function(e) { + ok(false, "Some test failed in " + script); + info(e); + info(e.message); + return Promise.resolve(); + }) + .then(function() { + if (parent && parent.finishTest) { + parent.finishTest(); + } else { + SimpleTest.finish(); + } + }); +} + diff --git a/dom/tests/mochitest/fetch/message_receiver.html b/dom/tests/mochitest/fetch/message_receiver.html new file mode 100644 index 000000000..82cb587c7 --- /dev/null +++ b/dom/tests/mochitest/fetch/message_receiver.html @@ -0,0 +1,6 @@ + + diff --git a/dom/tests/mochitest/fetch/mochitest.ini b/dom/tests/mochitest/fetch/mochitest.ini new file mode 100644 index 000000000..cf4477463 --- /dev/null +++ b/dom/tests/mochitest/fetch/mochitest.ini @@ -0,0 +1,57 @@ +[DEFAULT] +support-files = + fetch_test_framework.js + test_fetch_basic.js + test_fetch_basic_http.js + test_fetch_cors.js + test_formdataparsing.js + test_headers_common.js + test_request.js + test_response.js + utils.js + nested_worker_wrapper.js + worker_wrapper.js + message_receiver.html + reroute.html + reroute.js + reroute.js^headers^ + sw_reroute.js + empty.js + empty.js^headers^ + worker_temporaryFileBlob.js + common_temporaryFileBlob.js + !/dom/xhr/tests/file_XHR_binary1.bin + !/dom/xhr/tests/file_XHR_binary1.bin^headers^ + !/dom/xhr/tests/file_XHR_binary2.bin + !/dom/xhr/tests/file_XHR_pass1.xml + !/dom/xhr/tests/file_XHR_pass2.txt + !/dom/xhr/tests/file_XHR_pass3.txt + !/dom/xhr/tests/file_XHR_pass3.txt^headers^ + !/dom/xhr/tests/responseIdentical.sjs + !/dom/xhr/tests/temporaryFileBlob.sjs + !/dom/html/test/form_submit_server.sjs + !/dom/security/test/cors/file_CrossSiteXHR_server.sjs + +[test_headers.html] +[test_headers_sw_reroute.html] +[test_headers_mainthread.html] +[test_fetch_basic.html] +[test_fetch_basic_sw_reroute.html] +[test_fetch_basic_sw_empty_reroute.html] +[test_fetch_basic_http.html] +[test_fetch_basic_http_sw_reroute.html] +[test_fetch_basic_http_sw_empty_reroute.html] +[test_fetch_cors.html] +skip-if = toolkit == 'android' # Bug 1210282 +[test_fetch_cors_sw_reroute.html] +skip-if = toolkit == 'android' # Bug 1210282 +[test_fetch_cors_sw_empty_reroute.html] +skip-if = toolkit == 'android' # Bug 1210282 +[test_formdataparsing.html] +[test_formdataparsing_sw_reroute.html] +[test_request.html] +[test_request_context.html] +[test_request_sw_reroute.html] +[test_response.html] +[test_response_sw_reroute.html] +[test_temporaryFileBlob.html] diff --git a/dom/tests/mochitest/fetch/nested_worker_wrapper.js b/dom/tests/mochitest/fetch/nested_worker_wrapper.js new file mode 100644 index 000000000..7e463e7df --- /dev/null +++ b/dom/tests/mochitest/fetch/nested_worker_wrapper.js @@ -0,0 +1,28 @@ +// Hold the nested worker alive until this parent worker closes. +var worker; + +addEventListener('message', function nestedWorkerWrapperOnMessage(evt) { + removeEventListener('message', nestedWorkerWrapperOnMessage); + + worker = new Worker('worker_wrapper.js'); + + worker.addEventListener('message', function(evt) { + self.postMessage({ + context: 'NestedWorker', + type: evt.data.type, + status: evt.data.status, + msg: evt.data.msg, + }); + }); + + worker.addEventListener('error', function(evt) { + self.postMessage({ + context: 'NestedWorker', + type: 'status', + status: false, + msg: 'Nested worker error: ' + evt.message, + }); + }); + + worker.postMessage(evt.data); +}); diff --git a/dom/tests/mochitest/fetch/reroute.html b/dom/tests/mochitest/fetch/reroute.html new file mode 100644 index 000000000..bb12212ea --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.html @@ -0,0 +1,18 @@ + + + + + diff --git a/dom/tests/mochitest/fetch/reroute.js b/dom/tests/mochitest/fetch/reroute.js new file mode 100644 index 000000000..2c9bcd35f --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js @@ -0,0 +1,24 @@ +onfetch = function(e) { + if (e.request.url.indexOf("Referer") >= 0) { + // Silently rewrite the referrer so the referrer test passes since the + // document/worker isn't aware of this service worker. + var url = e.request.url.substring(0, e.request.url.indexOf('?')); + url += '?headers=' + ({ 'Referer': self.location.href }).toSource(); + + e.respondWith(e.request.text().then(function(text) { + var body = text === '' ? undefined : text; + var mode = e.request.mode == 'navigate' ? 'same-origin' : e.request.mode; + return fetch(url, { + method: e.request.method, + headers: e.request.headers, + body: body, + mode: mode, + credentials: e.request.credentials, + redirect: e.request.redirect, + cache: e.request.cache, + }); + })); + return; + } + e.respondWith(fetch(e.request)); +}; diff --git a/dom/tests/mochitest/fetch/reroute.js^headers^ b/dom/tests/mochitest/fetch/reroute.js^headers^ new file mode 100644 index 000000000..d0b9633bb --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: / diff --git a/dom/tests/mochitest/fetch/sw_reroute.js b/dom/tests/mochitest/fetch/sw_reroute.js new file mode 100644 index 000000000..11ad7b76e --- /dev/null +++ b/dom/tests/mochitest/fetch/sw_reroute.js @@ -0,0 +1,31 @@ +var gRegistration; + +function testScript(script) { + function setupSW(registration) { + gRegistration = registration; + + var iframe = document.createElement("iframe"); + iframe.src = "reroute.html?" + script.replace(".js", ""); + document.body.appendChild(iframe); + } + + SpecialPowers.pushPrefEnv({ + "set": [["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true]] + }, function() { + navigator.serviceWorker.ready.then(setupSW); + var scriptURL = location.href.includes("sw_empty_reroute.html") + ? "empty.js" : "reroute.js"; + navigator.serviceWorker.register(scriptURL, {scope: "/"}); + }); +} + +function finishTest() { + gRegistration.unregister().then(SimpleTest.finish, function(e) { + dump("unregistration failed: " + e + "\n"); + SimpleTest.finish(); + }); +} + +SimpleTest.waitForExplicitFinish(); diff --git a/dom/tests/mochitest/fetch/test_fetch_basic.html b/dom/tests/mochitest/fetch/test_fetch_basic.html new file mode 100644 index 000000000..ce7b63aba --- /dev/null +++ b/dom/tests/mochitest/fetch/test_fetch_basic.html @@ -0,0 +1,23 @@ + + + + + Bug 1039846 - Test fetch() function in worker + + + + +

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic.js b/dom/tests/mochitest/fetch/test_fetch_basic.js
new file mode 100644
index 000000000..16ce9d8a3
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic.js
@@ -0,0 +1,104 @@
+function testAboutURL() {
+  var p1 = fetch('about:blank').then(function(res) {
+    is(res.status, 200, "about:blank should load a valid Response");
+    is(res.headers.get('content-type'), 'text/html;charset=utf-8',
+       "about:blank content-type should be text/html;charset=utf-8");
+    is(res.headers.has('content-length'), false,
+       "about:blank should not have a content-length header");
+    return res.text().then(function(v) {
+      is(v, "", "about:blank body should be empty");
+    });
+  });
+
+  var p2 = fetch('about:config').then(function(res) {
+    ok(false, "about:config should fail");
+  }, function(e) {
+    ok(e instanceof TypeError, "about:config should fail");
+  });
+
+  return Promise.all([p1, p2]);
+}
+
+function testDataURL() {
+  return Promise.all(
+    [
+      ["data:text/plain;charset=UTF-8,Hello", 'text/plain;charset=UTF-8', 'Hello'],
+      ["data:text/plain;charset=utf-8;base64,SGVsbG8=", 'text/plain;charset=utf-8', 'Hello'],
+      ['data:text/xml,%3Cres%3Ehello%3C/res%3E%0A', 'text/xml', 'hello\n'],
+      ['data:text/plain,hello%20pass%0A', 'text/plain', 'hello pass\n'],
+      ['data:,foo', 'text/plain;charset=US-ASCII', 'foo'],
+      ['data:text/plain;base64,Zm9v', 'text/plain', 'foo'],
+      ['data:text/plain,foo#bar', 'text/plain', 'foo'],
+      ['data:text/plain,foo%23bar', 'text/plain', 'foo#bar'],
+    ].map(test => {
+      var uri = test[0], contentType = test[1], expectedBody = test[2];
+      return fetch(uri).then(res => {
+        ok(true, "Data URL fetch should resolve");
+        if (res.type == "error") {
+          ok(false, "Data URL fetch should not fail.");
+          return Promise.reject();
+        }
+        ok(res instanceof Response, "Fetch should resolve to a Response");
+        is(res.status, 200, "Data URL status should be 200");
+        is(res.statusText, "OK", "Data URL statusText should be OK");
+        ok(res.headers.has("content-type"), "Headers must have Content-Type header");
+        is(res.headers.get("content-type"), contentType, "Content-Type header should match specified value");
+        return res.text().then(body => is(body, expectedBody, "Data URL Body should match"));
+      })
+    })
+  );
+}
+
+function testSameOriginBlobURL() {
+  var blob = new Blob(["english ", "sentence"], { type: "text/plain" });
+  var url = URL.createObjectURL(blob);
+  return fetch(url).then(function(res) {
+    URL.revokeObjectURL(url);
+    ok(true, "Blob URL fetch should resolve");
+    if (res.type == "error") {
+      ok(false, "Blob URL fetch should not fail.");
+      return Promise.reject();
+    }
+    ok(res instanceof Response, "Fetch should resolve to a Response");
+    is(res.status, 200, "Blob fetch status should be 200");
+    is(res.statusText, "OK", "Blob fetch statusText should be OK");
+    ok(res.headers.has("content-type"), "Headers must have Content-Type header");
+    is(res.headers.get("content-type"), blob.type, "Content-Type header should match specified value");
+    ok(res.headers.has("content-length"), "Headers must have Content-Length header");
+    is(parseInt(res.headers.get("content-length")), 16, "Content-Length should match Blob's size");
+    return res.text().then(function(body) {
+      is(body, "english sentence", "Blob fetch body should match");
+    });
+  });
+}
+
+function testNonGetBlobURL() {
+  var blob = new Blob(["english ", "sentence"], { type: "text/plain" });
+  var url = URL.createObjectURL(blob);
+  return Promise.all(
+    [
+      "HEAD",
+      "POST",
+      "PUT",
+      "DELETE"
+    ].map(method => {
+      var req = new Request(url, { method: method });
+      return fetch(req).then(function(res) {
+        ok(false, "Blob URL with non-GET request should not succeed");
+      }).catch(function(e) {
+        ok(e instanceof TypeError, "Blob URL with non-GET request should get a TypeError");
+      });
+    })
+  ).then(function() {
+    URL.revokeObjectURL(url);
+  });
+}
+
+function runTest() {
+  return Promise.resolve()
+    .then(testAboutURL)
+    .then(testDataURL)
+    .then(testSameOriginBlobURL)
+    .then(testNonGetBlobURL)
+    // Put more promise based tests here.
+}
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http.html b/dom/tests/mochitest/fetch/test_fetch_basic_http.html
new file mode 100644
index 000000000..24175bb19
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Bug 1039846 - Test fetch() http fetching in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http.js b/dom/tests/mochitest/fetch/test_fetch_basic_http.js
new file mode 100644
index 000000000..4cd326ea8
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http.js
@@ -0,0 +1,201 @@
+var path = "/tests/dom/xhr/tests/";
+
+var passFiles = [['file_XHR_pass1.xml', 'GET', 200, 'OK', 'text/xml'],
+                 ['file_XHR_pass2.txt', 'GET', 200, 'OK', 'text/plain'],
+                 ['file_XHR_pass3.txt', 'GET', 200, 'OK', 'text/plain'],
+                 ];
+
+function testURL() {
+  var promises = [];
+  passFiles.forEach(function(entry) {
+    var p = fetch(path + entry[0]).then(function(res) {
+      ok(res.type !== "error", "Response should not be an error for " + entry[0]);
+      is(res.status, entry[2], "Status should match expected for " + entry[0]);
+      is(res.statusText, entry[3], "Status text should match expected for " + entry[0]);
+      // This file redirects to pass2, but that is invisible if a SW is present.
+      if (entry[0] != "file_XHR_pass3.txt" || isSWPresent)
+        ok(res.url.endsWith(path + entry[0]), "Response url should match request for simple fetch for " + entry[0]);
+      else
+        ok(res.url.endsWith(path + "file_XHR_pass2.txt"), "Response url should match request for simple fetch for " + entry[0]);
+      is(res.headers.get('content-type'), entry[4], "Response should have content-type for " + entry[0]);
+    });
+    promises.push(p);
+  });
+
+  return Promise.all(promises);
+}
+
+var failFiles = [['ftp://localhost' + path + 'file_XHR_pass1.xml', 'GET']];
+
+function testURLFail() {
+  var promises = [];
+  failFiles.forEach(function(entry) {
+    var p = fetch(entry[0]).then(function(res) {
+      ok(false, "Response should be an error for " + entry[0]);
+    }, function(e) {
+      ok(e instanceof TypeError, "Response should be an error for " + entry[0]);
+    });
+    promises.push(p);
+  });
+
+  return Promise.all(promises);
+}
+
+function testRequestGET() {
+  var promises = [];
+  passFiles.forEach(function(entry) {
+    var req = new Request(path + entry[0], { method: entry[1] });
+    var p = fetch(req).then(function(res) {
+      ok(res.type !== "error", "Response should not be an error for " + entry[0]);
+      is(res.status, entry[2], "Status should match expected for " + entry[0]);
+      is(res.statusText, entry[3], "Status text should match expected for " + entry[0]);
+      // This file redirects to pass2, but that is invisible if a SW is present.
+      if (entry[0] != "file_XHR_pass3.txt" || isSWPresent)
+        ok(res.url.endsWith(path + entry[0]), "Response url should match request for simple fetch for " + entry[0]);
+      else
+        ok(res.url.endsWith(path + "file_XHR_pass2.txt"), "Response url should match request for simple fetch for " + entry[0]);
+      is(res.headers.get('content-type'), entry[4], "Response should have content-type for " + entry[0]);
+    });
+    promises.push(p);
+  });
+
+  return Promise.all(promises);
+}
+
+function arraybuffer_equals_to(ab, s) {
+  is(ab.byteLength, s.length, "arraybuffer byteLength should match");
+
+  var u8v = new Uint8Array(ab);
+  is(String.fromCharCode.apply(String, u8v), s, "arraybuffer bytes should match");
+}
+
+function testResponses() {
+  var fetches = [
+    fetch(path + 'file_XHR_pass2.txt').then((res) => {
+      is(res.status, 200, "status should match");
+      return res.text().then((v) => is(v, "hello pass\n", "response should match"));
+    }),
+
+    fetch(path + 'file_XHR_binary1.bin').then((res) => {
+      is(res.status, 200, "status should match");
+      return res.arrayBuffer().then((v) =>
+        arraybuffer_equals_to(v, "\xaa\xee\0\x03\xff\xff\xff\xff\xbb\xbb\xbb\xbb")
+      )
+    }),
+
+    new Promise((resolve, reject) => {
+      var jsonBody = JSON.stringify({title: "aBook", author: "john"});
+      var req = new Request(path + 'responseIdentical.sjs', {
+                              method: 'POST',
+                              body: jsonBody,
+                            });
+      var p = fetch(req).then((res) => {
+        is(res.status, 200, "status should match");
+        return res.json().then((v) => {
+          is(JSON.stringify(v), jsonBody, "json response should match");
+        });
+      });
+      resolve(p);
+    }),
+
+    new Promise((resolve, reject) => {
+      var req = new Request(path + 'responseIdentical.sjs', {
+                              method: 'POST',
+                              body: '{',
+                            });
+      var p = fetch(req).then((res) => {
+        is(res.status, 200, "wrong status");
+        return res.json().then(
+          (v) => ok(false, "expected json parse failure"),
+          (e) => ok(true, "expected json parse failure")
+        );
+      });
+      resolve(p);
+    }),
+  ];
+
+  return Promise.all(fetches);
+}
+
+function testBlob() {
+  return fetch(path + '/file_XHR_binary2.bin').then((r) => {
+    ok(r.status, 200, "status should match");
+    return r.blob().then((b) => {
+      ok(b.size, 65536, "blob should have size 65536");
+      return readAsArrayBuffer(b).then(function(ab) {
+        var u8 = new Uint8Array(ab);
+        for (var i = 0; i < 65536; i++) {
+          if (u8[i] !== (i & 255)) {
+            break;
+          }
+        }
+        is(i, 65536, "wrong value at offset " + i);
+      });
+    });
+  });
+}
+
+// This test is a copy of dom/html/test/formData_test.js testSend() modified to
+// use the fetch API. Please change this if you change that.
+function testFormDataSend() {
+  var file, blob = new Blob(['hey'], {type: 'text/plain'});
+
+  var fd = new FormData();
+  fd.append("string", "hey");
+  fd.append("empty", blob);
+  fd.append("explicit", blob, "explicit-file-name");
+  fd.append("explicit-empty", blob, "");
+  file = new File([blob], 'testname',  {type: 'text/plain'});
+  fd.append("file-name", file);
+  file = new File([blob], '',  {type: 'text/plain'});
+  fd.append("empty-file-name", file);
+  file = new File([blob], 'testname',  {type: 'text/plain'});
+  fd.append("file-name-overwrite", file, "overwrite");
+
+  var req = new Request("/tests/dom/html/test/form_submit_server.sjs", {
+                          method: 'POST',
+                          body: fd,
+                        });
+
+  return fetch(req).then((r) => {
+    ok(r.status, 200, "status should match");
+    return r.json().then((response) => {
+      for (var entry of response) {
+        if (entry.headers['Content-Disposition'] != 'form-data; name="string"') {
+          is(entry.headers['Content-Type'], 'text/plain');
+        }
+
+        is(entry.body, 'hey');
+      }
+
+      is(response[1].headers['Content-Disposition'],
+          'form-data; name="empty"; filename="blob"');
+
+      is(response[2].headers['Content-Disposition'],
+          'form-data; name="explicit"; filename="explicit-file-name"');
+
+      is(response[3].headers['Content-Disposition'],
+          'form-data; name="explicit-empty"; filename=""');
+
+      is(response[4].headers['Content-Disposition'],
+          'form-data; name="file-name"; filename="testname"');
+
+      is(response[5].headers['Content-Disposition'],
+          'form-data; name="empty-file-name"; filename=""');
+
+      is(response[6].headers['Content-Disposition'],
+          'form-data; name="file-name-overwrite"; filename="overwrite"');
+    });
+  });
+}
+
+function runTest() {
+  return Promise.resolve()
+    .then(testURL)
+    .then(testURLFail)
+    .then(testRequestGET)
+    .then(testResponses)
+    .then(testBlob)
+    .then(testFormDataSend)
+    // Put more promise based tests here.
+}
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html
new file mode 100644
index 000000000..0f5052eda
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test fetch() http fetching in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html
new file mode 100644
index 000000000..0f5052eda
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test fetch() http fetching in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html
new file mode 100644
index 000000000..bcf959add
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test fetch() function in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html
new file mode 100644
index 000000000..bcf959add
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test fetch() function in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_cors.html b/dom/tests/mochitest/fetch/test_fetch_cors.html
new file mode 100644
index 000000000..b9a6992a5
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Bug 1039846 - Test fetch() CORS mode
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_cors.js b/dom/tests/mochitest/fetch/test_fetch_cors.js
new file mode 100644
index 000000000..ac83d050d
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors.js
@@ -0,0 +1,1748 @@
+var path = "/tests/dom/base/test/";
+
+function isOpaqueResponse(response) {
+  return response.type == "opaque" && response.status === 0 && response.statusText === "";
+}
+
+function testModeSameOrigin() {
+  // Fetch spec Section 4, step 4, "request's mode is same-origin".
+  var req = new Request("http://example.com", { mode: "same-origin" });
+  return fetch(req).then(function(res) {
+    ok(false, "Attempting to fetch a resource from a different origin with mode same-origin should fail.");
+  }, function(e) {
+    ok(e instanceof TypeError, "Attempting to fetch a resource from a different origin with mode same-origin should fail.");
+  });
+}
+
+function testNoCorsCtor() {
+  // Request constructor Step 19.1
+  var simpleMethods = ["GET", "HEAD", "POST"];
+  for (var i = 0; i < simpleMethods.length; ++i) {
+    var r = new Request("http://example.com", { method: simpleMethods[i], mode: "no-cors" });
+    ok(true, "no-cors Request with simple method " + simpleMethods[i] + " is allowed.");
+  }
+
+  var otherMethods = ["DELETE", "OPTIONS", "PUT"];
+  for (var i = 0; i < otherMethods.length; ++i) {
+    try {
+      var r = new Request("http://example.com", { method: otherMethods[i], mode: "no-cors" });
+      ok(false, "no-cors Request with non-simple method " + otherMethods[i] + " is not allowed.");
+    } catch(e) {
+      ok(true, "no-cors Request with non-simple method " + otherMethods[i] + " is not allowed.");
+    }
+  }
+
+  // Request constructor Step 19.2, check guarded headers.
+  var r = new Request(".", { mode: "no-cors" });
+  r.headers.append("Content-Type", "multipart/form-data");
+  is(r.headers.get("content-type"), "multipart/form-data", "Appending simple header should succeed");
+  r.headers.append("custom", "value");
+  ok(!r.headers.has("custom"), "Appending custom header should fail");
+  r.headers.append("DNT", "value");
+  ok(!r.headers.has("DNT"), "Appending forbidden header should fail");
+}
+
+var corsServerPath = "/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?";
+function testModeNoCors() {
+  // Fetch spec, section 4, step 4, response tainting should be set opaque, so
+  // that fetching leads to an opaque filtered response in step 8.
+  var r = new Request("http://example.com" + corsServerPath + "status=200", { mode: "no-cors" });
+  return fetch(r).then(function(res) {
+    ok(isOpaqueResponse(res), "no-cors Request fetch should result in opaque response");
+  }, function(e) {
+    ok(false, "no-cors Request fetch should not error");
+  });
+}
+
+function testSameOriginCredentials() {
+  var cookieStr = "type=chocolatechip";
+  var tests = [
+              {
+                // Initialize by setting a cookie.
+                pass: 1,
+                setCookie: cookieStr,
+                withCred: "same-origin",
+              },
+              {
+                // Default mode is "omit".
+                pass: 1,
+                noCookie: 1,
+              },
+              {
+                pass: 1,
+                noCookie: 1,
+                withCred: "omit",
+              },
+              {
+                pass: 1,
+                cookie: cookieStr,
+                withCred: "same-origin",
+              },
+              {
+                pass: 1,
+                cookie: cookieStr,
+                withCred: "include",
+              },
+              ];
+
+  var finalPromiseResolve, finalPromiseReject;
+  var finalPromise = new Promise(function(res, rej) {
+    finalPromiseResolve = res;
+    finalPromiseReject = rej;
+  });
+
+  function makeRequest(test) {
+    req = {
+      // Add a default query param just to make formatting the actual params
+      // easier.
+      url: corsServerPath + "a=b",
+      method: test.method,
+      headers: test.headers,
+      withCred: test.withCred,
+    };
+
+    if (test.setCookie)
+      req.url += "&setCookie=" + escape(test.setCookie);
+    if (test.cookie)
+      req.url += "&cookie=" + escape(test.cookie);
+    if (test.noCookie)
+      req.url += "&noCookie";
+
+    return new Request(req.url, { method: req.method,
+                                  headers: req.headers,
+                                  credentials: req.withCred });
+  }
+
+  function testResponse(res, test) {
+    ok(test.pass, "Expected test to pass " + test.toSource());
+    is(res.status, 200, "wrong status in test for " + test.toSource());
+    is(res.statusText, "OK", "wrong status text for " + test.toSource());
+    return res.text().then(function(v) {
+      is(v, "hello pass\n",
+       "wrong text in test for " + test.toSource());
+    });
+  }
+
+  function runATest(tests, i) {
+    var test = tests[i];
+    var request = makeRequest(test);
+    console.log(request.url);
+    fetch(request).then(function(res) {
+      testResponse(res, test).then(function() {
+        if (i < tests.length-1) {
+          runATest(tests, i+1);
+        } else {
+          finalPromiseResolve();
+        }
+      });
+    }, function(e) {
+      ok(!test.pass, "Expected test to fail " + test.toSource());
+      ok(e instanceof TypeError, "Test should fail " + test.toSource());
+      if (i < tests.length-1) {
+        runATest(tests, i+1);
+      } else {
+        finalPromiseResolve();
+      }
+    });
+  }
+
+  runATest(tests, 0);
+  return finalPromise;
+}
+
+function testModeCors() {
+  var tests = [// Plain request
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+               },
+
+               // undefined username
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 username: undefined
+               },
+
+               // undefined username and password
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 username: undefined,
+                 password: undefined
+               },
+
+               // nonempty username
+               { pass: 0,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 username: "user",
+               },
+
+               // nonempty password
+               // XXXbz this passes for now, because we ignore passwords
+               // without usernames in most cases.
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 password: "password",
+               },
+
+               // Default allowed headers
+               { pass: 1,
+                 method: "GET",
+                 headers: { "Content-Type": "text/plain",
+                            "Accept": "foo/bar",
+                            "Accept-Language": "sv-SE" },
+                 noAllowPreflight: 1,
+               },
+
+               { pass: 0,
+                 method: "GET",
+                 headers: { "Content-Type": "foo/bar",
+                            "Accept": "foo/bar",
+                            "Accept-Language": "sv-SE" },
+                 noAllowPreflight: 1,
+               },
+
+               { pass: 0,
+                 method: "GET",
+                 headers: { "Content-Type": "foo/bar, text/plain" },
+                 noAllowPreflight: 1,
+               },
+
+               { pass: 0,
+                 method: "GET",
+                 headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+                 noAllowPreflight: 1,
+               },
+
+               // Custom headers
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "X-My-Header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header": "secondValue" },
+                 allowHeaders: "x-my-header, long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header-long-header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my%-header": "myValue" },
+                 allowHeaders: "x-my%-header",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "" },
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "y-my-header",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header y-my-header",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header, y-my-header z",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header, y-my-he(ader",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "myheader": "" },
+                 allowMethods: "myheader",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "User-Agent": "myValue" },
+                 allowHeaders: "User-Agent",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "User-Agent": "myValue" },
+               },
+
+               // Multiple custom headers
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "second-header": "secondValue",
+                            "third-header": "thirdValue" },
+                 allowHeaders: "x-my-header, second-header, third-header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "second-header": "secondValue",
+                            "third-header": "thirdValue" },
+                 allowHeaders: "x-my-header,second-header,third-header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "second-header": "secondValue",
+                            "third-header": "thirdValue" },
+                 allowHeaders: "x-my-header ,second-header ,third-header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "second-header": "secondValue",
+                            "third-header": "thirdValue" },
+                 allowHeaders: "x-my-header , second-header , third-header",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "second-header": "secondValue" },
+                 allowHeaders: ",  x-my-header, , ,, second-header, ,   ",
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "second-header": "secondValue" },
+                 allowHeaders: "x-my-header, second-header, unused-header",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "myValue",
+                            "y-my-header": "secondValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "",
+                            "y-my-header": "" },
+                 allowHeaders: "x-my-header",
+               },
+
+               // HEAD requests
+               { pass: 1,
+                 method: "HEAD",
+                 noAllowPreflight: 1,
+               },
+
+               // HEAD with safe headers
+               { pass: 1,
+                 method: "HEAD",
+                 headers: { "Content-Type": "text/plain",
+                            "Accept": "foo/bar",
+                            "Accept-Language": "sv-SE" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "Content-Type": "foo/bar",
+                            "Accept": "foo/bar",
+                            "Accept-Language": "sv-SE" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "Content-Type": "foo/bar, text/plain" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+                 noAllowPreflight: 1,
+               },
+
+               // HEAD with custom headers
+               { pass: 1,
+                 method: "HEAD",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "x-my-header": "myValue" },
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "",
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "y-my-header",
+               },
+               { pass: 0,
+                 method: "HEAD",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header y-my-header",
+               },
+
+               // POST tests
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 noAllowPreflight: 1,
+               },
+               { pass: 1,
+                 method: "POST",
+               },
+               { pass: 1,
+                 method: "POST",
+                 noAllowPreflight: 1,
+               },
+
+               // POST with standard headers
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "text/plain" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "multipart/form-data" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "application/x-www-form-urlencoded" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar" },
+               },
+               { pass: 0,
+                 method: "POST",
+                 headers: { "Content-Type": "foo/bar" },
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "text/plain",
+                            "Accept": "foo/bar",
+                            "Accept-Language": "sv-SE" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar, text/plain" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar, text/plain, garbage" },
+                 noAllowPreflight: 1,
+               },
+
+               // POST with custom headers
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Accept": "foo/bar",
+                            "Accept-Language": "sv-SE",
+                            "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 1,
+                 method: "POST",
+                 headers: { "Content-Type": "text/plain",
+                            "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "text/plain",
+                            "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar",
+                            "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header, content-type",
+               },
+               { pass: 0,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar" },
+                 noAllowPreflight: 1,
+               },
+               { pass: 0,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar",
+                            "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 1,
+                 method: "POST",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header",
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "x-my-header": "myValue" },
+                 allowHeaders: "x-my-header, $_%",
+               },
+
+               // Other methods
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "DELETE",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowHeaders: "DELETE",
+               },
+               { pass: 0,
+                 method: "DELETE",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "POST, PUT, DELETE",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "POST, DELETE, PUT",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "DELETE, POST, PUT",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "POST ,PUT ,DELETE",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "POST,PUT,DELETE",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "POST , PUT , DELETE",
+               },
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "  ,,  PUT ,,  ,    , DELETE  ,  ,",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "PUT",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "DELETEZ",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "DELETE PUT",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "DELETE, PUT Z",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "DELETE, PU(T",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "PUT DELETE",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "PUT Z, DELETE",
+               },
+               { pass: 0,
+                 method: "DELETE",
+                 allowMethods: "PU(T, DELETE",
+               },
+               { pass: 0,
+                 method: "PUT",
+                 allowMethods: "put",
+               },
+
+               // Status messages
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 status: 404,
+                 statusMessage: "nothin' here",
+               },
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 status: 401,
+                 statusMessage: "no can do",
+               },
+               { pass: 1,
+                 method: "POST",
+                 body: "hi there",
+                 headers: { "Content-Type": "foo/bar" },
+                 allowHeaders: "content-type",
+                 status: 500,
+                 statusMessage: "server boo",
+               },
+               { pass: 1,
+                 method: "GET",
+                 noAllowPreflight: 1,
+                 status: 200,
+                 statusMessage: "Yes!!",
+               },
+               { pass: 0,
+                 method: "GET",
+                 headers: { "x-my-header": "header value" },
+                 allowHeaders: "x-my-header",
+                 preflightStatus: 400
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "header value" },
+                 allowHeaders: "x-my-header",
+                 preflightStatus: 200
+               },
+               { pass: 1,
+                 method: "GET",
+                 headers: { "x-my-header": "header value" },
+                 allowHeaders: "x-my-header",
+                 preflightStatus: 204
+               },
+
+               // exposed headers
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header" },
+                 exposeHeaders: "x-my-header",
+                 expectedResponseHeaders: ["x-my-header"],
+               },
+               { pass: 0,
+                 method: "GET",
+                 origin: "http://invalid",
+                 responseHeaders: { "x-my-header": "x header" },
+                 exposeHeaders: "x-my-header",
+                 expectedResponseHeaders: [],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header" },
+                 expectedResponseHeaders: [],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header" },
+                 exposeHeaders: "x-my-header y",
+                 expectedResponseHeaders: [],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header" },
+                 exposeHeaders: "y x-my-header",
+                 expectedResponseHeaders: [],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header" },
+                 exposeHeaders: "x-my-header, y-my-header z",
+                 expectedResponseHeaders: [],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header" },
+                 exposeHeaders: "x-my-header, y-my-hea(er",
+                 expectedResponseHeaders: [],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "x-my-header": "x header",
+                                    "y-my-header": "y header" },
+                 exposeHeaders: "  ,  ,,y-my-header,z-my-header,  ",
+                 expectedResponseHeaders: ["y-my-header"],
+               },
+               { pass: 1,
+                 method: "GET",
+                 responseHeaders: { "Cache-Control": "cacheControl header",
+                                    "Content-Language": "contentLanguage header",
+                                    "Expires":"expires header",
+                                    "Last-Modified":"lastModified header",
+                                    "Pragma":"pragma header",
+                                    "Unexpected":"unexpected header" },
+                 expectedResponseHeaders: ["Cache-Control","Content-Language","Content-Type","Expires","Last-Modified","Pragma"],
+               },
+               // Check that sending a body in the OPTIONS response works
+               { pass: 1,
+                 method: "DELETE",
+                 allowMethods: "DELETE",
+                 preflightBody: "I'm a preflight response body",
+               },
+               ];
+
+  var baseURL = "http://example.org" + corsServerPath;
+  var origin = "http://mochi.test:8888";
+  var fetches = [];
+  for (test of tests) {
+    var req = {
+      url: baseURL + "allowOrigin=" + escape(test.origin || origin),
+      method: test.method,
+      headers: test.headers,
+      uploadProgress: test.uploadProgress,
+      body: test.body,
+      responseHeaders: test.responseHeaders,
+    };
+
+    if (test.pass) {
+       req.url += "&origin=" + escape(origin) +
+                  "&requestMethod=" + test.method;
+    }
+
+    if ("username" in test) {
+      var u = new URL(req.url);
+      u.username = test.username || "";
+      req.url = u.href;
+    }
+
+    if ("password" in test) {
+      var u = new URL(req.url);
+      u.password = test.password || "";
+      req.url = u.href;
+    }
+
+    if (test.noAllowPreflight)
+      req.url += "&noAllowPreflight";
+
+    if (test.pass && "headers" in test) {
+      function isUnsafeHeader(name) {
+        lName = name.toLowerCase();
+        return lName != "accept" &&
+               lName != "accept-language" &&
+               (lName != "content-type" ||
+                ["text/plain",
+                 "multipart/form-data",
+                 "application/x-www-form-urlencoded"]
+                   .indexOf(test.headers[name].toLowerCase()) == -1);
+      }
+      req.url += "&headers=" + escape(test.headers.toSource());
+      reqHeaders =
+        escape(Object.keys(test.headers)
+               .filter(isUnsafeHeader)
+               .map(String.toLowerCase)
+               .sort()
+               .join(","));
+      req.url += reqHeaders ? "&requestHeaders=" + reqHeaders : "";
+    }
+    if ("allowHeaders" in test)
+      req.url += "&allowHeaders=" + escape(test.allowHeaders);
+    if ("allowMethods" in test)
+      req.url += "&allowMethods=" + escape(test.allowMethods);
+    if (test.body)
+      req.url += "&body=" + escape(test.body);
+    if (test.status) {
+      req.url += "&status=" + test.status;
+      req.url += "&statusMessage=" + escape(test.statusMessage);
+    }
+    if (test.preflightStatus)
+      req.url += "&preflightStatus=" + test.preflightStatus;
+    if (test.responseHeaders)
+      req.url += "&responseHeaders=" + escape(test.responseHeaders.toSource());
+    if (test.exposeHeaders)
+      req.url += "&exposeHeaders=" + escape(test.exposeHeaders);
+    if (test.preflightBody)
+      req.url += "&preflightBody=" + escape(test.preflightBody);
+
+    fetches.push((function(test) {
+      return new Promise(function(resolve) {
+        resolve(new Request(req.url, { method: req.method, mode: "cors",
+                                         headers: req.headers, body: req.body }));
+      }).then(function(request) {
+        return fetch(request);
+      }).then(function(res) {
+        ok(test.pass, "Expected test to pass for " + test.toSource());
+        if (test.status) {
+          is(res.status, test.status, "wrong status in test for " + test.toSource());
+          is(res.statusText, test.statusMessage, "wrong status text for " + test.toSource());
+        }
+        else {
+          is(res.status, 200, "wrong status in test for " + test.toSource());
+          is(res.statusText, "OK", "wrong status text for " + test.toSource());
+        }
+        if (test.responseHeaders) {
+          for (header in test.responseHeaders) {
+            if (test.expectedResponseHeaders.indexOf(header) == -1) {
+              is(res.headers.has(header), false,
+                 "|Headers.has()|wrong response header (" + header + ") in test for " +
+                 test.toSource());
+            }
+            else {
+              is(res.headers.get(header), test.responseHeaders[header],
+                 "|Headers.get()|wrong response header (" + header + ") in test for " +
+                 test.toSource());
+            }
+          }
+        }
+
+        return res.text();
+      }).then(function(v) {
+        if (test.method !== "HEAD") {
+          is(v, "hello pass\n",
+             "wrong responseText in test for " + test.toSource());
+        }
+        else {
+          is(v, "",
+             "wrong responseText in HEAD test for " + test.toSource());
+        }
+      }).catch(function(e) {
+        ok(!test.pass, "Expected test failure for " + test.toSource());
+        ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource());
+      });
+    })(test));
+  }
+
+  return Promise.all(fetches);
+}
+
+function testCrossOriginCredentials() {
+  var origin = "http://mochi.test:8888";
+  var tests = [
+           { pass: 1,
+             method: "GET",
+             withCred: "include",
+             allowCred: 1,
+           },
+           { pass: 0,
+             method: "GET",
+             withCred: "include",
+             allowCred: 0,
+           },
+           { pass: 0,
+             method: "GET",
+             withCred: "include",
+             allowCred: 1,
+             origin: "*",
+           },
+           { pass: 1,
+             method: "GET",
+             withCred: "omit",
+             allowCred: 1,
+             origin: "*",
+           },
+           { pass: 1,
+             method: "GET",
+             setCookie: "a=1",
+             withCred: "include",
+             allowCred: 1,
+           },
+           { pass: 1,
+             method: "GET",
+             cookie: "a=1",
+             withCred: "include",
+             allowCred: 1,
+           },
+           { pass: 1,
+             method: "GET",
+             noCookie: 1,
+             withCred: "omit",
+             allowCred: 1,
+           },
+           { pass: 0,
+             method: "GET",
+             noCookie: 1,
+             withCred: "include",
+             allowCred: 1,
+           },
+           { pass: 1,
+             method: "GET",
+             setCookie: "a=2",
+             withCred: "omit",
+             allowCred: 1,
+           },
+           { pass: 1,
+             method: "GET",
+             cookie: "a=1",
+             withCred: "include",
+             allowCred: 1,
+           },
+           { pass: 1,
+             method: "GET",
+             setCookie: "a=2",
+             withCred: "include",
+             allowCred: 1,
+           },
+           { pass: 1,
+             method: "GET",
+             cookie: "a=2",
+             withCred: "include",
+             allowCred: 1,
+           },
+           {
+             // When credentials mode is same-origin, but mode is cors, no
+             // cookie should be sent cross origin.
+             pass: 0,
+             method: "GET",
+             cookie: "a=2",
+             withCred: "same-origin",
+             allowCred: 1,
+           },
+           {
+             // When credentials mode is same-origin, but mode is cors, no
+             // cookie should be sent cross origin. This test checks the same
+             // thing as above, but uses the noCookie check on the server
+             // instead, and expects a valid response.
+             pass: 1,
+             method: "GET",
+             noCookie: 1,
+             withCred: "same-origin",
+           },
+           {
+             // Initialize by setting a cookies for same- and cross- origins.
+             pass: 1,
+             hops: [{ server: origin,
+                      setCookie: escape("a=1"),
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      allowCred: 1,
+                      setCookie: escape("a=2"),
+                    },
+                    ],
+             withCred: "include",
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      noCookie: 1,
+                    },
+                    ],
+             withCred: "same-origin",
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      allowCred: 1,
+                      cookie: escape("a=2"),
+                    },
+                    ],
+             withCred: "include",
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: '*',
+                      noCookie: 1,
+                    },
+                    ],
+             withCred: "same-origin",
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: '*',
+                      allowCred: 1,
+                      cookie: escape("a=2"),
+                    },
+                    ],
+             withCred: "include",
+           },
+           // fails because allow-credentials CORS header is not set by server
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: origin,
+                      cookie: escape("a=1"),
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      cookie: escape("a=2"),
+                    },
+                    ],
+             withCred: "include",
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: origin,
+                      noCookie: 1,
+                    },
+                    { server: origin,
+                      noCookie: 1,
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      noCookie: 1,
+                    },
+                    ],
+             withCred: "omit",
+           },
+           ];
+
+  var baseURL = "http://example.org" + corsServerPath;
+  var origin = "http://mochi.test:8888";
+
+  var finalPromiseResolve, finalPromiseReject;
+  var finalPromise = new Promise(function(res, rej) {
+    finalPromiseResolve = res;
+    finalPromiseReject = rej;
+  });
+
+  function makeRequest(test) {
+    var url;
+    if (test.hops) {
+      url = test.hops[0].server + corsServerPath + "hop=1&hops=" +
+            escape(test.hops.toSource());
+    } else {
+      url = baseURL + "allowOrigin=" + escape(test.origin || origin);
+    }
+    req = {
+      url: url,
+      method: test.method,
+      headers: test.headers,
+      withCred: test.withCred,
+    };
+
+    if (test.allowCred)
+      req.url += "&allowCred";
+
+    if (test.setCookie)
+      req.url += "&setCookie=" + escape(test.setCookie);
+    if (test.cookie)
+      req.url += "&cookie=" + escape(test.cookie);
+    if (test.noCookie)
+      req.url += "&noCookie";
+
+    if ("allowHeaders" in test)
+      req.url += "&allowHeaders=" + escape(test.allowHeaders);
+    if ("allowMethods" in test)
+      req.url += "&allowMethods=" + escape(test.allowMethods);
+
+    return new Request(req.url, { method: req.method,
+                                  headers: req.headers,
+                                  credentials: req.withCred });
+  }
+
+  function testResponse(res, test) {
+    ok(test.pass, "Expected test to pass for " + test.toSource());
+    is(res.status, 200, "wrong status in test for " + test.toSource());
+    is(res.statusText, "OK", "wrong status text for " + test.toSource());
+    return res.text().then(function(v) {
+      is(v, "hello pass\n",
+       "wrong text in test for " + test.toSource());
+    });
+  }
+
+  function runATest(tests, i) {
+    var test = tests[i];
+    var request = makeRequest(test);
+    fetch(request).then(function(res) {
+      testResponse(res, test).then(function() {
+        if (i < tests.length-1) {
+          runATest(tests, i+1);
+        } else {
+          finalPromiseResolve();
+        }
+      });
+    }, function(e) {
+      ok(!test.pass, "Expected test failure for " + test.toSource());
+      ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource());
+      if (i < tests.length-1) {
+        runATest(tests, i+1);
+      } else {
+        finalPromiseResolve();
+      }
+    });
+  }
+
+  runATest(tests, 0);
+  return finalPromise;
+}
+
+function testModeNoCorsCredentials() {
+  var cookieStr = "type=chocolatechip";
+  var tests = [
+              {
+                // Initialize by setting a cookie.
+                pass: 1,
+                setCookie: cookieStr,
+                withCred: "include",
+              },
+              {
+                pass: 1,
+                noCookie: 1,
+                withCred: "omit",
+              },
+              {
+                pass: 1,
+                noCookie: 1,
+                withCred: "same-origin",
+              },
+              {
+                pass: 1,
+                cookie: cookieStr,
+                withCred: "include",
+              },
+              {
+                pass: 1,
+                cookie: cookieStr,
+                withCred: "omit",
+                status: 500,
+              },
+              {
+                pass: 1,
+                cookie: cookieStr,
+                withCred: "same-origin",
+                status: 500,
+              },
+              {
+                pass: 1,
+                noCookie: 1,
+                withCred: "include",
+                status: 500,
+              },
+              ];
+
+  var finalPromiseResolve, finalPromiseReject;
+  var finalPromise = new Promise(function(res, rej) {
+    finalPromiseResolve = res;
+    finalPromiseReject = rej;
+  });
+
+  function makeRequest(test) {
+    req = {
+      url : "http://example.org" + corsServerPath + "a+b",
+      withCred: test.withCred,
+    };
+
+    if (test.setCookie)
+      req.url += "&setCookie=" + escape(test.setCookie);
+    if (test.cookie)
+      req.url += "&cookie=" + escape(test.cookie);
+    if (test.noCookie)
+      req.url += "&noCookie";
+
+    return new Request(req.url, { method: 'GET',
+                                  mode: 'no-cors',
+                                  credentials: req.withCred });
+  }
+
+  function testResponse(res, test) {
+    is(res.type, 'opaque', 'wrong response type for ' + test.toSource());
+
+    // Get unfiltered response
+    var chromeResponse = SpecialPowers.wrap(res);
+    var unfiltered = chromeResponse.cloneUnfiltered();
+
+    var status = test.status ? test.status : 200;
+    is(unfiltered.status, status, "wrong status in test for " + test.toSource());
+    return unfiltered.text().then(function(v) {
+      if (status === 200) {
+        is(v, "hello pass\n",
+         "wrong text in test for " + test.toSource());
+      }
+    });
+  }
+
+  function runATest(tests, i) {
+    if (typeof SpecialPowers !== 'object') {
+      finalPromiseResolve();
+      return;
+    }
+
+    var test = tests[i];
+    var request = makeRequest(test);
+    fetch(request).then(function(res) {
+      testResponse(res, test).then(function() {
+        if (i < tests.length-1) {
+          runATest(tests, i+1);
+        } else {
+          finalPromiseResolve();
+        }
+      });
+    }, function(e) {
+      ok(!test.pass, "Expected test to fail " + test.toSource());
+      ok(e instanceof TypeError, "Test should fail " + test.toSource());
+      if (i < tests.length-1) {
+        runATest(tests, i+1);
+      } else {
+        finalPromiseResolve();
+      }
+    });
+  }
+
+  runATest(tests, 0);
+  return finalPromise;
+}
+
+function testCORSRedirects() {
+  var origin = "http://mochi.test:8888";
+
+  var tests = [
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://mochi.test:8888",
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://mochi.test:8888",
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://test2.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    { server: "http://sub2.xn--lt-uia.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://test2.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    { server: "http://sub2.xn--lt-uia.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://test2.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    { server: "http://sub2.xn--lt-uia.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://test2.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    { server: "http://sub2.xn--lt-uia.mochi.test:8888",
+                      allowOrigin: "x"
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://test2.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    { server: "http://sub2.xn--lt-uia.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin
+                    },
+                    { server: "http://test2.mochi.test:8888",
+                      allowOrigin: origin
+                    },
+                    { server: "http://sub2.xn--lt-uia.mochi.test:8888",
+                      allowOrigin: "*"
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain" },
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain",
+                        "my-header": "myValue",
+                      },
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain",
+                        "my-header": "myValue",
+                      },
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                      noAllowPreflight: 1,
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain",
+                        "my-header": "myValue",
+                      },
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://test1.example.com",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                    },
+                    { server: "http://test2.example.com",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                    }
+                    ],
+           },
+           { pass: 1,
+             method: "DELETE",
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      allowMethods: "DELETE",
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "DELETE",
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                      allowMethods: "DELETE",
+                      noAllowPreflight: 1,
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "DELETE",
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://test1.example.com",
+                      allowOrigin: origin,
+                      allowMethods: "DELETE",
+                    },
+                    { server: "http://test2.example.com",
+                      allowOrigin: origin,
+                      allowMethods: "DELETE",
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain",
+                        "my-header": "myValue",
+                      },
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin,
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: origin,
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "DELETE",
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin,
+                      allowMethods: "DELETE",
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: origin,
+                      allowMethods: "DELETE",
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain",
+                        "my-header": "myValue",
+                      },
+             hops: [{ server: "http://example.com",
+                    },
+                    { server: "http://sub1.test1.mochi.test:8888",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain" },
+             hops: [{ server: "http://mochi.test:8888",
+                    },
+                    { server: "http://example.com",
+                      allowOrigin: origin,
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "POST",
+             body: "hi there",
+             headers: { "Content-Type": "text/plain",
+                        "my-header": "myValue",
+                      },
+             hops: [{ server: "http://example.com",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                    },
+                    { server: "http://mochi.test:8888",
+                      allowOrigin: origin,
+                      allowHeaders: "my-header",
+                    },
+                    ],
+           },
+           ];
+
+  var fetches = [];
+  for (test of tests) {
+    req = {
+      url: test.hops[0].server + corsServerPath + "hop=1&hops=" +
+           escape(test.hops.toSource()),
+      method: test.method,
+      headers: test.headers,
+      body: test.body,
+    };
+
+    if (test.headers) {
+      req.url += "&headers=" + escape(test.headers.toSource());
+    }
+
+    if (test.pass) {
+      if (test.body)
+        req.url += "&body=" + escape(test.body);
+    }
+
+    var request = new Request(req.url, { method: req.method,
+                                         headers: req.headers,
+                                         body: req.body });
+    fetches.push((function(request, test) {
+      return fetch(request).then(function(res) {
+        ok(test.pass, "Expected test to pass for " + test.toSource());
+        is(res.status, 200, "wrong status in test for " + test.toSource());
+        is(res.statusText, "OK", "wrong status text for " + test.toSource());
+        is(res.type, 'cors', 'wrong response type for ' + test.toSource());
+        var reqHost = (new URL(req.url)).host;
+        // If there is a service worker present, the redirections will be
+        // transparent, assuming that the original request is to the current
+        // site and would be intercepted.
+        if (isSWPresent) {
+          if (reqHost === location.host) {
+            is((new URL(res.url)).host, reqHost, "Response URL should be original URL with a SW present");
+          }
+        } else {
+          is((new URL(res.url)).host, (new URL(test.hops[test.hops.length-1].server)).host, "Response URL should be redirected URL");
+        }
+        return res.text().then(function(v) {
+          is(v, "hello pass\n",
+             "wrong responseText in test for " + test.toSource());
+        });
+      }, function(e) {
+        ok(!test.pass, "Expected test failure for " + test.toSource());
+        ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource());
+      });
+    })(request, test));
+  }
+
+  return Promise.all(fetches);
+}
+
+function testNoCORSRedirects() {
+  var origin = "http://mochi.test:8888";
+
+  var tests = [
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: "http://example.com",
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: origin,
+                    },
+                    { server: "http://example.com",
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "GET",
+             // Must use a simple header due to no-cors header restrictions.
+             headers: { "accept-language": "en-us",
+                      },
+             hops: [{ server: origin,
+                    },
+                    { server: "http://example.com",
+                    },
+                    ],
+           },
+           { pass: 1,
+             method: "GET",
+             hops: [{ server: origin,
+                    },
+                    { server: "http://example.com",
+                    },
+                    { server: origin,
+                    }
+                    ],
+           },
+           { pass: 1,
+             method: "POST",
+             body: 'upload body here',
+             hops: [{ server: origin
+                    },
+                    { server: "http://example.com",
+                    },
+                    ],
+           },
+           { pass: 0,
+             method: "DELETE",
+             hops: [{ server: origin
+                    },
+                    { server: "http://example.com",
+                    },
+                    ],
+           },
+           ];
+
+  var fetches = [];
+  for (test of tests) {
+    req = {
+      url: test.hops[0].server + corsServerPath + "hop=1&hops=" +
+           escape(test.hops.toSource()),
+      method: test.method,
+      headers: test.headers,
+      body: test.body,
+    };
+
+    if (test.headers) {
+      req.url += "&headers=" + escape(test.headers.toSource());
+    }
+
+    if (test.pass) {
+      if (test.body)
+        req.url += "&body=" + escape(test.body);
+    }
+
+    fetches.push((function(req, test) {
+      return new Promise(function(resolve, reject) {
+        resolve(new Request(req.url, { mode: 'no-cors',
+                                       method: req.method,
+                                       headers: req.headers,
+                                       body: req.body }));
+      }).then(function(request) {
+        return fetch(request);
+      }).then(function(res) {
+        ok(test.pass, "Expected test to pass for " + test.toSource());
+        // All requests are cross-origin no-cors, we should always have
+        // an opaque response here.  All values on the opaque response
+        // should be hidden.
+        is(res.type, 'opaque', 'wrong response type for ' + test.toSource());
+        is(res.status, 0, "wrong status in test for " + test.toSource());
+        is(res.statusText, "", "wrong status text for " + test.toSource());
+        is(res.url, '', 'wrong response url for ' + test.toSource());
+        return res.text().then(function(v) {
+          is(v, "", "wrong responseText in test for " + test.toSource());
+        });
+      }, function(e) {
+        ok(!test.pass, "Expected test failure for " + test.toSource());
+        ok(e instanceof TypeError, "Exception should be TypeError for " + test.toSource());
+      });
+    })(req, test));
+  }
+
+  return Promise.all(fetches);
+}
+
+function testReferrer() {
+  var referrer;
+  if (self && self.location) {
+    referrer = self.location.href;
+  } else {
+    referrer = document.documentURI;
+  }
+
+  var dict = {
+    'Referer': referrer
+  };
+  return fetch(corsServerPath + "headers=" + dict.toSource()).then(function(res) {
+    is(res.status, 200, "expected correct referrer header to be sent");
+    dump(res.statusText);
+  }, function(e) {
+    ok(false, "expected correct referrer header to be sent");
+  });
+}
+
+function runTest() {
+  testNoCorsCtor();
+
+  return Promise.resolve()
+    .then(testModeSameOrigin)
+    .then(testModeNoCors)
+    .then(testModeCors)
+    .then(testSameOriginCredentials)
+    .then(testCrossOriginCredentials)
+    .then(testModeNoCorsCredentials)
+    .then(testCORSRedirects)
+    .then(testNoCORSRedirects)
+    .then(testReferrer)
+    // Put more promise based tests here.
+}
diff --git a/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html b/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html
new file mode 100644
index 000000000..7ad368cfd
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test fetch() CORS mode
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html b/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html
new file mode 100644
index 000000000..7ad368cfd
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test fetch() CORS mode
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_formdataparsing.html b/dom/tests/mochitest/fetch/test_formdataparsing.html
new file mode 100644
index 000000000..ca81311fd
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_formdataparsing.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Bug 1109751 - Test FormData parsing
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_formdataparsing.js b/dom/tests/mochitest/fetch/test_formdataparsing.js
new file mode 100644
index 000000000..bc7eb21a6
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_formdataparsing.js
@@ -0,0 +1,283 @@
+var boundary = "1234567891011121314151617";
+
+// fn(body) should create a Body subclass with content body treated as
+// FormData and return it.
+function testFormDataParsing(fn) {
+
+  function makeTest(shouldPass, input, testFn) {
+    var obj = fn(input);
+    return obj.formData().then(function(fd) {
+      ok(shouldPass, "Expected test to be valid FormData for " + input);
+      if (testFn) {
+        return testFn(fd);
+      }
+    }, function(e) {
+      if (shouldPass) {
+        ok(false, "Expected test to pass for " + input);
+      } else {
+        ok(e.name == "TypeError", "Error should be a TypeError.");
+      }
+    });
+  }
+
+  // [shouldPass?, input, testFn]
+  var tests =
+    [
+      [ true,
+
+        boundary +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          is(fd.get("greeting"), '"hello"');
+        }
+      ],
+      [ false,
+
+        // Invalid disposition.
+        boundary +
+        '\r\nContent-Disposition: form-datafoobar; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ true,
+
+        '--' +
+        boundary +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          is(fd.get("greeting"), '"hello"');
+        }
+      ],
+      [ false,
+        boundary + "\r\n\r\n" + boundary + '-',
+      ],
+      [ false,
+        // No valid ending.
+        boundary + "\r\n\r\n" + boundary,
+      ],
+      [ false,
+
+        // One '-' prefix is not allowed. 2 or none.
+        '-' +
+        boundary +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        'invalid' +
+        boundary +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary + 'suffix' +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary + 'suffix' +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        // Partial boundary
+        boundary.substr(3) +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // Missing '\n' at beginning.
+        '\rContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // No form-data.
+        '\r\nContent-Disposition: mixed; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // No headers.
+        '\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // No content-disposition.
+        '\r\nContent-Dispositypo: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // No name.
+        '\r\nContent-Disposition: form-data;\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // Missing empty line between headers and body.
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        // Empty entry followed by valid entry.
+        boundary + "\r\n\r\n" + boundary +
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+
+        boundary +
+        // Header followed by empty line, but empty body not followed by
+        // newline.
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n' +
+        boundary + '-',
+      ],
+      [ true,
+
+        boundary +
+        // Empty body followed by newline.
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          is(fd.get("greeting"), "", "Empty value is allowed.");
+        }
+      ],
+      [ false,
+        boundary +
+        // Value is boundary itself.
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n' +
+        boundary + '\r\n' +
+        boundary + '-',
+      ],
+      [ false,
+        boundary +
+        // Variant of above with no valid ending boundary.
+        '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n' +
+        boundary
+      ],
+      [ true,
+        boundary +
+        // Unquoted filename with empty body.
+        '\r\nContent-Disposition: form-data; name="file"; filename=file1.txt\r\n\r\n\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          var f = fd.get("file");
+          ok(f instanceof File, "Entry with filename attribute should be read as File.");
+          is(f.name, "file1.txt", "Filename should match.");
+          is(f.type, "text/plain", "Default content-type should be text/plain.");
+          return readAsText(f).then(function(text) {
+            is(text, "", "File should be empty.");
+          });
+        }
+      ],
+      [ true,
+        boundary +
+        // Quoted filename with empty body.
+        '\r\nContent-Disposition: form-data; name="file"; filename="file1.txt"\r\n\r\n\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          var f = fd.get("file");
+          ok(f instanceof File, "Entry with filename attribute should be read as File.");
+          is(f.name, "file1.txt", "Filename should match.");
+          is(f.type, "text/plain", "Default content-type should be text/plain.");
+          return readAsText(f).then(function(text) {
+            is(text, "", "File should be empty.");
+          });
+        }
+      ],
+      [ false,
+        boundary +
+        // Invalid filename
+        '\r\nContent-Disposition: form-data; name="file"; filename="[\n@;xt"\r\n\r\n\r\n' +
+        boundary + '-',
+      ],
+      [ true,
+        boundary +
+        '\r\nContent-Disposition: form-data; name="file"; filename="[@;xt"\r\n\r\n\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          var f = fd.get("file");
+          ok(f instanceof File, "Entry with filename attribute should be read as File.");
+          is(f.name, "[@", "Filename should match.");
+        }
+      ],
+      [ true,
+        boundary +
+        '\r\nContent-Disposition: form-data; name="file"; filename="file with   spaces"\r\n\r\n\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          var f = fd.get("file");
+          ok(f instanceof File, "Entry with filename attribute should be read as File.");
+          is(f.name, "file with spaces", "Filename should match.");
+        }
+      ],
+      [ true,
+        boundary + '\r\n' +
+        'Content-Disposition: form-data; name="file"; filename="xml.txt"\r\n' +
+        'content-type       : application/xml\r\n' +
+        '\r\n' +
+        'foobar\r\n\r\n\r\n' +
+        boundary + '-',
+
+        function(fd) {
+          var f = fd.get("file");
+          ok(f instanceof File, "Entry with filename attribute should be read as File.");
+          is(f.name, "xml.txt", "Filename should match.");
+          is(f.type, "application/xml", "content-type should be application/xml.");
+          return readAsText(f).then(function(text) {
+            is(text, "foobar\r\n\r\n", "File should have correct text.");
+          });
+        }
+      ],
+    ];
+
+  var promises = [];
+  for (var i = 0; i < tests.length; ++i) {
+    var test = tests[i];
+    promises.push(makeTest(test[0], test[1], test[2]));
+  }
+
+  return Promise.all(promises);
+}
+
+function makeRequest(body) {
+  var req = new Request("", { method: 'post', body: body,
+                              headers: {
+                                'Content-Type': 'multipart/form-data; boundary=' + boundary
+                              }});
+  return req;
+}
+
+function makeResponse(body) {
+  var res = new Response(body, { headers: {
+                                   'Content-Type': 'multipart/form-data; boundary=' + boundary
+                                 }});
+  return res;
+}
+
+function runTest() {
+  return Promise.all([testFormDataParsing(makeRequest),
+                      testFormDataParsing(makeResponse)]);
+}
diff --git a/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html b/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html
new file mode 100644
index 000000000..b3fe0db44
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1109751 - Test FormData parsing
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_headers.html b/dom/tests/mochitest/fetch/test_headers.html
new file mode 100644
index 000000000..f13f53425
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_headers.html
@@ -0,0 +1,17 @@
+
+
+
+
+  Test Fetch Headers - Basic
+  
+  
+
+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_headers_common.js b/dom/tests/mochitest/fetch/test_headers_common.js
new file mode 100644
index 000000000..fe792b25b
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_headers_common.js
@@ -0,0 +1,229 @@
+//
+// Utility functions
+//
+
+function shouldThrow(func, expected, msg) {
+  var err;
+  try {
+    func();
+  } catch(e) {
+    err = e;
+  } finally {
+    ok(err instanceof expected, msg);
+  }
+}
+
+function recursiveArrayCompare(actual, expected) {
+  is(Array.isArray(actual), Array.isArray(expected), "Both should either be arrays, or not");
+  if (Array.isArray(actual) && Array.isArray(expected)) {
+    var diff = actual.length !== expected.length;
+
+    for (var i = 0, n = actual.length; !diff && i < n; ++i) {
+      diff = recursiveArrayCompare(actual[i], expected[i]);
+    }
+
+    return diff;
+  } else {
+    return actual !== expected;
+  }
+}
+
+function arrayEquals(actual, expected, msg) {
+  if (actual === expected) {
+    return;
+  }
+
+  var diff = recursiveArrayCompare(actual, expected);
+
+  ok(!diff, msg);
+  if (diff) {
+    is(actual, expected, msg);
+  }
+}
+
+function checkHas(headers, name, msg) {
+  function doCheckHas(n) {
+    return headers.has(n);
+  }
+  return _checkHas(doCheckHas, headers, name, msg);
+}
+
+function checkNotHas(headers, name, msg) {
+  function doCheckNotHas(n) {
+    return !headers.has(n);
+  }
+  return _checkHas(doCheckNotHas, headers, name, msg);
+}
+
+function _checkHas(func, headers, name, msg) {
+  ok(func(name), msg);
+  ok(func(name.toLowerCase()), msg)
+  ok(func(name.toUpperCase()), msg)
+}
+
+function checkGet(headers, name, expected, msg) {
+  is(headers.get(name), expected, msg);
+  is(headers.get(name.toLowerCase()), expected, msg);
+  is(headers.get(name.toUpperCase()), expected, msg);
+}
+
+//
+// Test Cases
+//
+
+function TestCoreBehavior(headers, name) {
+  var start = headers.get(name);
+
+  headers.append(name, "bar");
+
+  checkHas(headers, name, "Has the header");
+  var expected = (start ? start.concat(",bar") : "bar");
+  checkGet(headers, name, expected, "Retrieve all headers for name");
+
+  headers.append(name, "baz");
+  checkHas(headers, name, "Has the header");
+  expected = (start ? start.concat(",bar,baz") : "bar,baz");
+  checkGet(headers, name, expected, "Retrieve all headers for name");
+
+  headers.set(name, "snafu");
+  checkHas(headers, name, "Has the header after set");
+  checkGet(headers, name, "snafu", "Retrieve all headers after set");
+
+  headers.delete(name.toUpperCase());
+  checkNotHas(headers, name, "Does not have the header after delete");
+  checkGet(headers, name, null, "Retrieve all headers after delete");
+
+  // should be ok to delete non-existent name
+  headers.delete(name);
+
+  shouldThrow(function() {
+    headers.append("foo,", "bam");
+  }, TypeError, "Append invalid header name should throw TypeError.");
+
+  shouldThrow(function() {
+    headers.append(name, "bam\n");
+  }, TypeError, "Append invalid header value should throw TypeError.");
+
+  shouldThrow(function() {
+    headers.append(name, "bam\n\r");
+  }, TypeError, "Append invalid header value should throw TypeError.");
+
+  ok(!headers.guard, "guard should be undefined in content");
+}
+
+function TestEmptyHeaders() {
+  is(typeof Headers, "function", "Headers global constructor exists.");
+  var headers = new Headers();
+  ok(headers, "Constructed empty Headers object");
+  TestCoreBehavior(headers, "foo");
+}
+
+function TestFilledHeaders() {
+  var source = new Headers();
+  var filled = new Headers(source);
+  ok(filled, "Fill header from empty header");
+  TestCoreBehavior(filled, "foo");
+
+  source = new Headers();
+  source.append("abc", "123");
+  source.append("def", "456");
+  source.append("def", "789");
+
+  filled = new Headers(source);
+  checkGet(filled, "abc", source.get("abc"), "Single value header list matches");
+  checkGet(filled, "def", source.get("def"), "Multiple value header list matches");
+  TestCoreBehavior(filled, "def");
+
+  filled = new Headers({
+    "zxy": "987",
+    "xwv": "654",
+    "uts": "321"
+  });
+  checkGet(filled, "zxy", "987", "Has first object filled key");
+  checkGet(filled, "xwv", "654", "Has second object filled key");
+  checkGet(filled, "uts", "321", "Has third object filled key");
+  TestCoreBehavior(filled, "xwv");
+
+  filled = new Headers([
+    ["zxy", "987"],
+    ["xwv", "654"],
+    ["xwv", "abc"],
+    ["uts", "321"]
+  ]);
+  checkGet(filled, "zxy", "987", "Has first sequence filled key");
+  checkGet(filled, "xwv", "654,abc", "Has second sequence filled key");
+  checkGet(filled, "uts", "321", "Has third sequence filled key");
+  TestCoreBehavior(filled, "xwv");
+
+  shouldThrow(function() {
+    filled = new Headers([
+      ["zxy", "987", "654"],
+      ["uts", "321"]
+    ]);
+  }, TypeError, "Fill with non-tuple sequence should throw TypeError.");
+
+  shouldThrow(function() {
+    filled = new Headers([
+      ["zxy"],
+      ["uts", "321"]
+    ]);
+  }, TypeError, "Fill with non-tuple sequence should throw TypeError.");
+}
+
+function iterate(iter) {
+  var result = [];
+  for (var val = iter.next(); !val.done;) {
+    result.push(val.value);
+    val = iter.next();
+  }
+  return result;
+}
+
+function iterateForOf(iter) {
+  var result = [];
+  for (var value of iter) {
+    result.push(value);
+  }
+  return result;
+}
+
+function byteInflate(str) {
+  var encoder = new TextEncoder("utf-8");
+  var encoded = encoder.encode(str);
+  var result = "";
+  for (var i = 0; i < encoded.length; ++i) {
+    result += String.fromCharCode(encoded[i]);
+  }
+  return result
+}
+
+function TestHeadersIterator() {
+  var ehsanInflated = byteInflate("احسان");
+  var headers = new Headers();
+  headers.set("foo0", "bar0");
+  headers.append("foo", "bar");
+  headers.append("foo", ehsanInflated);
+  headers.append("Foo2", "bar2");
+  headers.set("Foo2", "baz2");
+  headers.set("foo3", "bar3");
+  headers.delete("foo0");
+  headers.delete("foo3");
+
+  var key_iter = headers.keys();
+  var value_iter = headers.values();
+  var entries_iter = headers.entries();
+
+  arrayEquals(iterate(key_iter), ["foo", "foo", "foo2"], "Correct key iterator");
+  arrayEquals(iterate(value_iter), ["bar", ehsanInflated, "baz2"], "Correct value iterator");
+  arrayEquals(iterate(entries_iter), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator");
+
+  arrayEquals(iterateForOf(headers), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator");
+  arrayEquals(iterateForOf(new Headers(headers)), [["foo", "bar"], ["foo", ehsanInflated], ["foo2", "baz2"]], "Correct entries iterator");
+}
+
+function runTest() {
+  TestEmptyHeaders();
+  TestFilledHeaders();
+  TestHeadersIterator();
+  return Promise.resolve();
+}
diff --git a/dom/tests/mochitest/fetch/test_headers_mainthread.html b/dom/tests/mochitest/fetch/test_headers_mainthread.html
new file mode 100644
index 000000000..9bdc89b71
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_headers_mainthread.html
@@ -0,0 +1,155 @@
+
+
+
+
+  Test Fetch Headers - Basic
+  
+  
+
+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_headers_sw_reroute.html b/dom/tests/mochitest/fetch/test_headers_sw_reroute.html
new file mode 100644
index 000000000..dd6ec5fb3
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_headers_sw_reroute.html
@@ -0,0 +1,16 @@
+
+
+
+
+  Test Fetch Headers - Basic
+  
+  
+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_request.html b/dom/tests/mochitest/fetch/test_request.html
new file mode 100644
index 000000000..83869fd11
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_request.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Test Request object in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_request.js b/dom/tests/mochitest/fetch/test_request.js
new file mode 100644
index 000000000..badad61b9
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_request.js
@@ -0,0 +1,542 @@
+function testDefaultCtor() {
+  var req = new Request("");
+  is(req.method, "GET", "Default Request method is GET");
+  ok(req.headers instanceof Headers, "Request should have non-null Headers object");
+  is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
+  is(req.context, "fetch", "Default context is fetch.");
+  is(req.referrer, "about:client", "Default referrer is `client` which serializes to about:client.");
+  is(req.mode, "cors", "Request mode for string input is cors");
+  is(req.credentials, "omit", "Default Request credentials is omit");
+  is(req.cache, "default", "Default Request cache is default");
+
+  var req = new Request(req);
+  is(req.method, "GET", "Default Request method is GET");
+  ok(req.headers instanceof Headers, "Request should have non-null Headers object");
+  is(req.url, self.location.href, "URL should be resolved with entry settings object's API base URL");
+  is(req.context, "fetch", "Default context is fetch.");
+  is(req.referrer, "about:client", "Default referrer is `client` which serializes to about:client.");
+  is(req.mode, "cors", "Request mode string input is cors");
+  is(req.credentials, "omit", "Default Request credentials is omit");
+  is(req.cache, "default", "Default Request cache is default");
+}
+
+function testClone() {
+  var orig = new Request("./cloned_request.txt", {
+              method: 'POST',
+              headers: { "Sample-Header": "5" },
+              body: "Sample body",
+              mode: "same-origin",
+              credentials: "same-origin",
+              cache: "no-store",
+            });
+  var clone = orig.clone();
+  ok(clone.method === "POST", "Request method is POST");
+  ok(clone.headers instanceof Headers, "Request should have non-null Headers object");
+
+  is(clone.headers.get('sample-header'), "5", "Request sample-header should be 5.");
+  orig.headers.set('sample-header', 6);
+  is(clone.headers.get('sample-header'), "5", "Cloned Request sample-header should continue to be 5.");
+
+  ok(clone.url === (new URL("./cloned_request.txt", self.location.href)).href,
+       "URL should be resolved with entry settings object's API base URL");
+  ok(clone.referrer === "about:client", "Default referrer is `client` which serializes to about:client.");
+  ok(clone.mode === "same-origin", "Request mode is same-origin");
+  ok(clone.credentials === "same-origin", "Default credentials is same-origin");
+  ok(clone.cache === "no-store", "Default cache is no-store");
+
+  ok(!orig.bodyUsed, "Original body is not consumed.");
+  ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+  var origBody = null;
+  var clone2 = null;
+  return orig.text().then(function (body) {
+    origBody = body;
+    is(origBody, "Sample body", "Original body string matches");
+    ok(orig.bodyUsed, "Original body is consumed.");
+    ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+    try {
+      orig.clone()
+      ok(false, "Cannot clone Request whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    clone2 = clone.clone();
+    return clone.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone.clone()
+      ok(false, "Cannot clone Request whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    return clone2.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone2.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone2.clone()
+      ok(false, "Cannot clone Request whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+  });
+}
+
+function testUsedRequest() {
+  // Passing a used request should fail.
+  var req = new Request("", { method: 'post', body: "This is foo" });
+  var p1 = req.text().then(function(v) {
+    try {
+      var req2 = new Request(req);
+      ok(false, "Used Request cannot be passed to new Request");
+    } catch(e) {
+      ok(true, "Used Request cannot be passed to new Request");
+    }
+  });
+
+  // Passing a request should set the request as used.
+  var reqA = new Request("", { method: 'post', body: "This is foo" });
+  var reqB = new Request(reqA);
+  is(reqA.bodyUsed, true, "Passing a Request to another Request should set the former as used");
+  return p1;
+}
+
+function testSimpleUrlParse() {
+  // Just checks that the URL parser is actually being used.
+  var req = new Request("/file.html");
+  is(req.url, (new URL("/file.html", self.location.href)).href, "URL parser should be used to resolve Request URL");
+}
+
+// Bug 1109574 - Passing a Request with null body should keep bodyUsed unset.
+function testBug1109574() {
+  var r1 = new Request("");
+  is(r1.bodyUsed, false, "Initial value of bodyUsed should be false");
+  var r2 = new Request(r1);
+  is(r1.bodyUsed, false, "Request with null body should not have bodyUsed set");
+  // This should succeed.
+  var r3 = new Request(r1);
+}
+
+// Bug 1184550 - Request constructor should always throw if used flag is set,
+// even if body is null
+function testBug1184550() {
+  var req = new Request("", { method: 'post', body: "Test" });
+  fetch(req);
+  ok(req.bodyUsed, "Request body should be used immediately after fetch()");
+  return fetch(req).then(function(resp) {
+    ok(false, "Second fetch with same request should fail.");
+  }).catch(function(err) {
+    is(err.name, 'TypeError', "Second fetch with same request should fail.");
+  });
+}
+
+function testHeaderGuard() {
+  var headers = {
+    "Cookie": "Custom cookie",
+    "Non-Simple-Header": "value",
+  };
+  var r1 = new Request("", { headers: headers });
+  ok(!r1.headers.has("Cookie"), "Default Request header should have guard request and prevent setting forbidden header.");
+  ok(r1.headers.has("Non-Simple-Header"), "Default Request header should have guard request and allow setting non-simple header.");
+
+  var r2 = new Request("", { mode: "no-cors", headers: headers });
+  ok(!r2.headers.has("Cookie"), "no-cors Request header should have guard request-no-cors and prevent setting non-simple header.");
+  ok(!r2.headers.has("Non-Simple-Header"), "no-cors Request header should have guard request-no-cors and prevent setting non-simple header.");
+}
+
+function testMode() {
+  try {
+    var req = new Request("http://example.com", {mode: "navigate"});
+    ok(false, "Creating a Request with navigate RequestMode should throw a TypeError");
+  } catch(e) {
+    is(e.name, "TypeError", "Creating a Request with navigate RequestMode should throw a TypeError");
+  }
+}
+
+function testMethod() {
+  // These get normalized.
+  var allowed = ["delete", "get", "head", "options", "post", "put" ];
+  for (var i = 0; i < allowed.length; ++i) {
+    try {
+      var r = new Request("", { method: allowed[i] });
+      ok(true, "Method " + allowed[i] + " should be allowed");
+      is(r.method, allowed[i].toUpperCase(),
+         "Standard HTTP method " + allowed[i] + " should be normalized");
+    } catch(e) {
+      ok(false, "Method " + allowed[i] + " should be allowed");
+    }
+  }
+
+  var allowed = [ "pAtCh", "foo" ];
+  for (var i = 0; i < allowed.length; ++i) {
+    try {
+      var r = new Request("", { method: allowed[i] });
+      ok(true, "Method " + allowed[i] + " should be allowed");
+      is(r.method, allowed[i],
+         "Non-standard but valid HTTP method " + allowed[i] +
+         " should not be normalized");
+    } catch(e) {
+      ok(false, "Method " + allowed[i] + " should be allowed");
+    }
+  }
+
+  var forbidden = ["connect", "trace", "track", " {
+    is(v, "Sample body", "Body should match");
+    is(req.bodyUsed, true, "After reading body, bodyUsed should be true.");
+  }).then((v) => {
+    return req.blob().then((v) => {
+      ok(false, "Attempting to read body again should fail.");
+    }, (e) => {
+      ok(true, "Attempting to read body again should fail.");
+    })
+  });
+}
+
+var text = "κόσμε";
+function testBodyCreation() {
+  var req1 = new Request("", { method: 'post', body: text });
+  var p1 = req1.text().then(function(v) {
+    ok(typeof v === "string", "Should resolve to string");
+    is(text, v, "Extracted string should match");
+  });
+
+  var req2 = new Request("", { method: 'post', body: new Uint8Array([72, 101, 108, 108, 111]) });
+  var p2 = req2.text().then(function(v) {
+    is("Hello", v, "Extracted string should match");
+  });
+
+  var req2b = new Request("", { method: 'post', body: (new Uint8Array([72, 101, 108, 108, 111])).buffer });
+  var p2b = req2b.text().then(function(v) {
+    is("Hello", v, "Extracted string should match");
+  });
+
+  var reqblob = new Request("", { method: 'post', body: new Blob([text]) });
+  var pblob = reqblob.text().then(function(v) {
+    is(v, text, "Extracted string should match");
+  });
+
+  // FormData has its own function since it has blobs and files.
+
+  var params = new URLSearchParams();
+  params.append("item", "Geckos");
+  params.append("feature", "stickyfeet");
+  params.append("quantity", "700");
+  var req3 = new Request("", { method: 'post', body: params });
+  var p3 = req3.text().then(function(v) {
+    var extracted = new URLSearchParams(v);
+    is(extracted.get("item"), "Geckos", "Param should match");
+    is(extracted.get("feature"), "stickyfeet", "Param should match");
+    is(extracted.get("quantity"), "700", "Param should match");
+  });
+
+  return Promise.all([p1, p2, p2b, pblob, p3]);
+}
+
+function testFormDataBodyCreation() {
+  var f1 = new FormData();
+  f1.append("key", "value");
+  f1.append("foo", "bar");
+
+  var r1 = new Request("", { method: 'post', body: f1 })
+  // Since f1 is serialized immediately, later additions should not show up.
+  f1.append("more", "stuff");
+  var p1 = r1.formData().then(function(fd) {
+    ok(fd instanceof FormData, "Valid FormData extracted.");
+    ok(fd.has("key"), "key should exist.");
+    ok(fd.has("foo"), "foo should exist.");
+    ok(!fd.has("more"), "more should not exist.");
+  });
+
+  f1.append("blob", new Blob([text]));
+  var r2 = new Request("", { method: 'post', body: f1 });
+  f1.delete("key");
+  var p2 = r2.formData().then(function(fd) {
+    ok(fd instanceof FormData, "Valid FormData extracted.");
+    ok(fd.has("more"), "more should exist.");
+
+    var b = fd.get("blob");
+    ok(b.name, "blob", "blob entry should be a Blob.");
+    ok(b instanceof Blob, "blob entry should be a Blob.");
+
+    return readAsText(b).then(function(output) {
+      is(output, text, "Blob contents should match.");
+    });
+  });
+
+  return Promise.all([p1, p2]);
+}
+
+function testBodyExtraction() {
+  var text = "κόσμε";
+  var newReq = function() { return new Request("", { method: 'post', body: text }); }
+  return newReq().text().then(function(v) {
+    ok(typeof v === "string", "Should resolve to string");
+    is(text, v, "Extracted string should match");
+  }).then(function() {
+    return newReq().blob().then(function(v) {
+      ok(v instanceof Blob, "Should resolve to Blob");
+      return readAsText(v).then(function(result) {
+        is(result, text, "Decoded Blob should match original");
+      });
+    });
+  }).then(function() {
+    return newReq().json().then(function(v) {
+      ok(false, "Invalid json should reject");
+    }, function(e) {
+      ok(true, "Invalid json should reject");
+    })
+  }).then(function() {
+    return newReq().arrayBuffer().then(function(v) {
+      ok(v instanceof ArrayBuffer, "Should resolve to ArrayBuffer");
+      var dec = new TextDecoder();
+      is(dec.decode(new Uint8Array(v)), text, "UTF-8 decoded ArrayBuffer should match original");
+    });
+  }).then(function() {
+    return newReq().formData().then(function(v) {
+      ok(false, "invalid FormData read should fail.");
+    }, function(e) {
+      ok(e.name == "TypeError", "invalid FormData read should fail.");
+    });
+  });
+}
+
+function testFormDataBodyExtraction() {
+  // URLSearchParams translates to application/x-www-form-urlencoded.
+  var params = new URLSearchParams();
+  params.append("item", "Geckos");
+  params.append("feature", "stickyfeet");
+  params.append("quantity", "700");
+  params.append("quantity", "800");
+
+  var req = new Request("", { method: 'POST', body: params });
+  var p1 = req.formData().then(function(fd) {
+    ok(fd.has("item"), "Has entry 'item'.");
+    ok(fd.has("feature"), "Has entry 'feature'.");
+    var entries = fd.getAll("quantity");
+    is(entries.length, 2, "Entries with same name are correctly handled.");
+    is(entries[0], "700", "Entries with same name are correctly handled.");
+    is(entries[1], "800", "Entries with same name are correctly handled.");
+  });
+
+  var f1 = new FormData();
+  f1.append("key", "value");
+  f1.append("foo", "bar");
+  f1.append("blob", new Blob([text]));
+  var r2 = new Request("", { method: 'post', body: f1 });
+  var p2 = r2.formData().then(function(fd) {
+    ok(fd.has("key"), "Has entry 'key'.");
+    ok(fd.has("foo"), "Has entry 'foo'.");
+    ok(fd.has("blob"), "Has entry 'blob'.");
+    var entries = fd.getAll("blob");
+    is(entries.length, 1, "getAll returns all items.");
+    is(entries[0].name, "blob", "Filename should be blob.");
+    ok(entries[0] instanceof Blob, "getAll returns blobs.");
+  });
+
+  var ws = "\r\n\r\n\r\n\r\n";
+  f1.set("key", new File([ws], 'file name has spaces.txt', { type: 'new/lines' }));
+  var r3 = new Request("", { method: 'post', body: f1 });
+  var p3 = r3.formData().then(function(fd) {
+    ok(fd.has("foo"), "Has entry 'foo'.");
+    ok(fd.has("blob"), "Has entry 'blob'.");
+    var entries = fd.getAll("blob");
+    is(entries.length, 1, "getAll returns all items.");
+    is(entries[0].name, "blob", "Filename should be blob.");
+    ok(entries[0] instanceof Blob, "getAll returns blobs.");
+
+    ok(fd.has("key"), "Has entry 'key'.");
+    var f = fd.get("key");
+    ok(f instanceof File, "entry should be a File.");
+    is(f.name, "file name has spaces.txt", "File name should match.");
+    is(f.type, "new/lines", "File type should match.");
+    is(f.size, ws.length, "File size should match.");
+    return readAsText(f).then(function(text) {
+      is(text, ws, "File contents should match.");
+    });
+  });
+
+  // Override header and ensure parse fails.
+  var boundary = "1234567891011121314151617";
+  var body = boundary +
+             '\r\nContent-Disposition: form-data; name="greeting"\r\n\r\n"hello"\r\n' +
+             boundary + '-';
+
+  var r4 = new Request("", { method: 'post', body: body, headers: {
+                               "Content-Type": "multipart/form-datafoobar; boundary="+boundary,
+                           }});
+  var p4 = r4.formData().then(function() {
+    ok(false, "Invalid mimetype should fail.");
+  }, function() {
+    ok(true, "Invalid mimetype should fail.");
+  });
+
+  var r5 = new Request("", { method: 'POST', body: params, headers: {
+                               "Content-Type": "application/x-www-form-urlencodedfoobar",
+                           }});
+  var p5 = r5.formData().then(function() {
+    ok(false, "Invalid mimetype should fail.");
+  }, function() {
+    ok(true, "Invalid mimetype should fail.");
+  });
+  return Promise.all([p1, p2, p3, p4]);
+}
+
+// mode cannot be set to "CORS-with-forced-preflight" from javascript.
+function testModeCorsPreflightEnumValue() {
+  try {
+    var r = new Request(".", { mode: "cors-with-forced-preflight" });
+    ok(false, "Creating Request with mode cors-with-forced-preflight should fail.");
+  } catch(e) {
+    ok(true, "Creating Request with mode cors-with-forced-preflight should fail.");
+    // Also ensure that the error message matches error messages for truly
+    // invalid strings.
+    var invalidMode = "not-in-requestmode-enum";
+    var invalidExc;
+    try {
+      var r = new Request(".", { mode: invalidMode });
+    } catch(e) {
+      invalidExc = e;
+    }
+    var expectedMessage = invalidExc.message.replace(invalidMode, 'cors-with-forced-preflight');
+    is(e.message, expectedMessage,
+       "mode cors-with-forced-preflight should throw same error as invalid RequestMode strings.");
+  }
+}
+
+// HEAD/GET Requests are not allowed to have a body even when copying another
+// Request.
+function testBug1154268() {
+  var r1 = new Request("/index.html", { method: "POST", body: "Hi there" });
+  ["HEAD", "GET"].forEach(function(method) {
+    try {
+      var r2 = new Request(r1, { method: method });
+      ok(false, method + " Request copied from POST Request with body should fail.");
+    } catch (e) {
+      is(e.name, "TypeError", method + " Request copied from POST Request with body should fail.");
+    }
+  });
+}
+
+function testRequestConsumedByFailedConstructor(){
+  var r1 = new Request('http://example.com', { method: 'POST', body: 'hello world' });
+  try{
+    var r2 = new Request(r1, { method: 'GET' });
+    ok(false, 'GET Request copied from POST Request with body should fail.');
+  } catch(e) {
+    ok(true, 'GET Request copied from POST Request with body should fail.');
+  }
+  ok(!r1.bodyUsed, 'Initial request should not be consumed by failed Request constructor');
+}
+
+function runTest() {
+  testDefaultCtor();
+  testSimpleUrlParse();
+  testUrlFragment();
+  testUrlCredentials();
+  testUrlMalformed();
+  testMode();
+  testMethod();
+  testBug1109574();
+  testBug1184550();
+  testHeaderGuard();
+  testModeCorsPreflightEnumValue();
+  testBug1154268();
+  testRequestConsumedByFailedConstructor();
+
+  return Promise.resolve()
+    .then(testBodyCreation)
+    .then(testBodyUsed)
+    .then(testBodyExtraction)
+    .then(testFormDataBodyCreation)
+    .then(testFormDataBodyExtraction)
+    .then(testUsedRequest)
+    .then(testClone())
+    // Put more promise based tests here.
+}
diff --git a/dom/tests/mochitest/fetch/test_request_context.html b/dom/tests/mochitest/fetch/test_request_context.html
new file mode 100644
index 000000000..07c99e2f0
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_request_context.html
@@ -0,0 +1,19 @@
+
+
+
+
+  Make sure that Request.context is not exposed by default
+  
+  
+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_request_sw_reroute.html b/dom/tests/mochitest/fetch/test_request_sw_reroute.html
new file mode 100644
index 000000000..9fbfe767c
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_request_sw_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Test Request object in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_response.html b/dom/tests/mochitest/fetch/test_response.html
new file mode 100644
index 000000000..648851e8b
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_response.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Bug 1039846 - Test Response object in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_response.js b/dom/tests/mochitest/fetch/test_response.js
new file mode 100644
index 000000000..e396184ee
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_response.js
@@ -0,0 +1,267 @@
+function testDefaultCtor() {
+  var res = new Response();
+  is(res.type, "default", "Default Response type is default");
+  ok(res.headers instanceof Headers, "Response should have non-null Headers object");
+  is(res.url, "", "URL should be empty string");
+  is(res.status, 200, "Default status is 200");
+  is(res.statusText, "OK", "Default statusText is OK");
+}
+
+function testClone() {
+  var orig = new Response("This is a body", {
+              status: 404,
+              statusText: "Not Found",
+              headers: { "Content-Length": 5 },
+            });
+  var clone = orig.clone();
+  is(clone.status, 404, "Response status is 404");
+  is(clone.statusText, "Not Found", "Response statusText is POST");
+  ok(clone.headers instanceof Headers, "Response should have non-null Headers object");
+
+  is(clone.headers.get('content-length'), "5", "Response content-length should be 5.");
+  orig.headers.set('content-length', 6);
+  is(clone.headers.get('content-length'), "5", "Response content-length should be 5.");
+
+  ok(!orig.bodyUsed, "Original body is not consumed.");
+  ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+  var origBody = null;
+  var clone2 = null;
+  return orig.text().then(function (body) {
+    origBody = body;
+    is(origBody, "This is a body", "Original body string matches");
+    ok(orig.bodyUsed, "Original body is consumed.");
+    ok(!clone.bodyUsed, "Clone body is not consumed.");
+
+    try {
+      orig.clone()
+      ok(false, "Cannot clone Response whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    clone2 = clone.clone();
+    return clone.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone.clone()
+      ok(false, "Cannot clone Response whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+
+    return clone2.text();
+  }).then(function (body) {
+    is(body, origBody, "Clone body matches original body.");
+    ok(clone2.bodyUsed, "Clone body is consumed.");
+
+    try {
+      clone2.clone()
+      ok(false, "Cannot clone Response whose body is already consumed");
+    } catch (e) {
+      is(e.name, "TypeError", "clone() of consumed body should throw TypeError");
+    }
+  });
+}
+
+function testCloneUnfiltered() {
+  var url = 'http://example.com/tests/dom/security/test/cors/file_CrossSiteXHR_server.sjs?status=200';
+  return fetch(url, { mode: 'no-cors' }).then(function(response) {
+    // By default the chrome-only function should not be available.
+    is(response.type, 'opaque', 'response should be opaque');
+    is(response.cloneUnfiltered, undefined,
+       'response.cloneUnfiltered should be undefined');
+
+    // When the test is run in a worker context we can't actually try to use
+    // the chrome-only function.  SpecialPowers is not defined.
+    if (typeof SpecialPowers !== 'object') {
+      return;
+    }
+
+    // With a chrome code, however, should be able to get an unfiltered response.
+    var chromeResponse = SpecialPowers.wrap(response);
+    is(typeof chromeResponse.cloneUnfiltered, 'function',
+       'chromeResponse.cloneFiltered should be a function');
+    var unfiltered = chromeResponse.cloneUnfiltered();
+    is(unfiltered.type, 'default', 'unfiltered response should be default');
+    is(unfiltered.status, 200, 'unfiltered response should have 200 status');
+  });
+}
+
+function testError() {
+  var res = Response.error();
+  is(res.status, 0, "Error response status should be 0");
+  try {
+    res.headers.set("someheader", "not allowed");
+    ok(false, "Error response should have immutable headers");
+  } catch(e) {
+    ok(true, "Error response should have immutable headers");
+  }
+}
+
+function testRedirect() {
+  var res = Response.redirect("./redirect.response");
+  is(res.status, 302, "Default redirect has status code 302");
+  var h = res.headers.get("location");
+  ok(h === (new URL("./redirect.response", self.location.href)).href, "Location header should be correct absolute URL");
+  try {
+    res.headers.set("someheader", "not allowed");
+    ok(false, "Redirects should have immutable headers");
+  } catch(e) {
+    ok(true, "Redirects should have immutable headers");
+  }
+
+  var successStatus = [301, 302, 303, 307, 308];
+  for (var i = 0; i < successStatus.length; ++i) {
+    var res = Response.redirect("./redirect.response", successStatus[i]);
+    is(res.status, successStatus[i], "Status code should match");
+  }
+
+  var failStatus = [300, 0, 304, 305, 306, 309, 500];
+  for (var i = 0; i < failStatus.length; ++i) {
+    try {
+      var res = Response.redirect(".", failStatus[i]);
+      ok(false, "Invalid status code should fail " + failStatus[i]);
+    } catch(e) {
+      is(e.name, "RangeError", "Invalid status code should fail " + failStatus[i]);
+    }
+  }
+}
+
+function testOk() {
+  var r1 = new Response("", { status: 200 });
+  ok(r1.ok, "Response with status 200 should have ok true");
+
+  var r2 = new Response(undefined, { status: 204 });
+  ok(r2.ok, "Response with status 204 should have ok true");
+
+  var r3 = new Response("", { status: 299 });
+  ok(r3.ok, "Response with status 299 should have ok true");
+
+  var r4 = new Response("", { status: 302 });
+  ok(!r4.ok, "Response with status 302 should have ok false");
+}
+
+function testBodyUsed() {
+  var res = new Response("Sample body");
+  ok(!res.bodyUsed, "bodyUsed is initially false.");
+  return res.text().then((v) => {
+    is(v, "Sample body", "Body should match");
+    ok(res.bodyUsed, "After reading body, bodyUsed should be true.");
+  }).then(() => {
+    return res.blob().then((v) => {
+      ok(false, "Attempting to read body again should fail.");
+    }, (e) => {
+      ok(true, "Attempting to read body again should fail.");
+    })
+  });
+}
+
+function testBodyCreation() {
+  var text = "κόσμε";
+  var res1 = new Response(text);
+  var p1 = res1.text().then(function(v) {
+    ok(typeof v === "string", "Should resolve to string");
+    is(text, v, "Extracted string should match");
+  });
+
+  var res2 = new Response(new Uint8Array([72, 101, 108, 108, 111]));
+  var p2 = res2.text().then(function(v) {
+    is("Hello", v, "Extracted string should match");
+  });
+
+  var res2b = new Response((new Uint8Array([72, 101, 108, 108, 111])).buffer);
+  var p2b = res2b.text().then(function(v) {
+    is("Hello", v, "Extracted string should match");
+  });
+
+  var resblob = new Response(new Blob([text]));
+  var pblob = resblob.text().then(function(v) {
+    is(v, text, "Extracted string should match");
+  });
+
+  var params = new URLSearchParams();
+  params.append("item", "Geckos");
+  params.append("feature", "stickyfeet");
+  params.append("quantity", "700");
+  var res3 = new Response(params);
+  var p3 = res3.text().then(function(v) {
+    var extracted = new URLSearchParams(v);
+    is(extracted.get("item"), "Geckos", "Param should match");
+    is(extracted.get("feature"), "stickyfeet", "Param should match");
+    is(extracted.get("quantity"), "700", "Param should match");
+  });
+
+  return Promise.all([p1, p2, p2b, pblob, p3]);
+}
+
+function testBodyExtraction() {
+  var text = "κόσμε";
+  var newRes = function() { return new Response(text); }
+  return newRes().text().then(function(v) {
+    ok(typeof v === "string", "Should resolve to string");
+    is(text, v, "Extracted string should match");
+  }).then(function() {
+    return newRes().blob().then(function(v) {
+      ok(v instanceof Blob, "Should resolve to Blob");
+      return readAsText(v).then(function(result) {
+        is(result, text, "Decoded Blob should match original");
+      });
+    });
+  }).then(function() {
+    return newRes().json().then(function(v) {
+      ok(false, "Invalid json should reject");
+    }, function(e) {
+      ok(true, "Invalid json should reject");
+    })
+  }).then(function() {
+    return newRes().arrayBuffer().then(function(v) {
+      ok(v instanceof ArrayBuffer, "Should resolve to ArrayBuffer");
+      var dec = new TextDecoder();
+      is(dec.decode(new Uint8Array(v)), text, "UTF-8 decoded ArrayBuffer should match original");
+    });
+  })
+}
+
+function testNullBodyStatus() {
+  [204, 205, 304].forEach(function(status) {
+    try {
+      var res = new Response(new Blob(), { "status": status });
+      ok(false, "Response body provided but status code does not permit a body");
+    } catch(e) {
+      ok(true, "Response body provided but status code does not permit a body");
+    }
+  });
+
+  [204, 205, 304].forEach(function(status) {
+    try {
+      var res = new Response(undefined, { "status": status });
+      ok(true, "Response body provided but status code does not permit a body");
+    } catch(e) {
+      ok(false, "Response body provided but status code does not permit a body");
+    }
+  });
+}
+
+function runTest() {
+  testDefaultCtor();
+  testError();
+  testRedirect();
+  testOk();
+  testNullBodyStatus();
+
+  return Promise.resolve()
+    .then(testBodyCreation)
+    .then(testBodyUsed)
+    .then(testBodyExtraction)
+    .then(testClone)
+    .then(testCloneUnfiltered)
+    // Put more promise based tests here.
+    .catch(function(e) {
+      dump('### ### ' + e + '\n');
+      ok(false, 'got unexpected error!');
+    });
+}
diff --git a/dom/tests/mochitest/fetch/test_response_sw_reroute.html b/dom/tests/mochitest/fetch/test_response_sw_reroute.html
new file mode 100644
index 000000000..9f9751b8a
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_response_sw_reroute.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1039846 - Test Response object in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_temporaryFileBlob.html b/dom/tests/mochitest/fetch/test_temporaryFileBlob.html
new file mode 100644
index 000000000..f7390f9d2
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_temporaryFileBlob.html
@@ -0,0 +1,35 @@
+
+
+
+  
+  Test for Bug 1312410
+  
+  
+  
+
+
+  
+
+
diff --git a/dom/tests/mochitest/fetch/utils.js b/dom/tests/mochitest/fetch/utils.js
new file mode 100644
index 000000000..5d294041e
--- /dev/null
+++ b/dom/tests/mochitest/fetch/utils.js
@@ -0,0 +1,37 @@
+// Utilities
+// =========
+
+// Helper that uses FileReader or FileReaderSync based on context and returns
+// a Promise that resolves with the text or rejects with error.
+function readAsText(blob) {
+  if (typeof FileReader !== "undefined") {
+    return new Promise(function(resolve, reject) {
+      var fs = new FileReader();
+      fs.onload = function() {
+        resolve(fs.result);
+      }
+      fs.onerror = reject;
+      fs.readAsText(blob);
+    });
+  } else {
+    var fs = new FileReaderSync();
+    return Promise.resolve(fs.readAsText(blob));
+  }
+}
+
+function readAsArrayBuffer(blob) {
+  if (typeof FileReader !== "undefined") {
+    return new Promise(function(resolve, reject) {
+      var fs = new FileReader();
+      fs.onload = function() {
+        resolve(fs.result);
+      }
+      fs.onerror = reject;
+      fs.readAsArrayBuffer(blob);
+    });
+  } else {
+    var fs = new FileReaderSync();
+    return Promise.resolve(fs.readAsArrayBuffer(blob));
+  }
+}
+
diff --git a/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js
new file mode 100644
index 000000000..182dc7c18
--- /dev/null
+++ b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js
@@ -0,0 +1,21 @@
+importScripts('common_temporaryFileBlob.js');
+
+function info(msg) {
+  postMessage({type: 'info', msg: msg});
+}
+
+function ok(a, msg) {
+  postMessage({type: 'check', what: !!a, msg: msg});
+}
+
+function is(a, b, msg) {
+  ok(a === b, msg);
+}
+
+function next() {
+  postMessage({type: 'finish'});
+}
+
+onmessage = function(e) {
+  test_basic();
+}
diff --git a/dom/tests/mochitest/fetch/worker_wrapper.js b/dom/tests/mochitest/fetch/worker_wrapper.js
new file mode 100644
index 000000000..c2f0ec00e
--- /dev/null
+++ b/dom/tests/mochitest/fetch/worker_wrapper.js
@@ -0,0 +1,58 @@
+importScripts("utils.js");
+var client;
+var context;
+
+function ok(a, msg) {
+  client.postMessage({type: 'status', status: !!a,
+                      msg: a + ": " + msg, context: context});
+}
+
+function is(a, b, msg) {
+  client.postMessage({type: 'status', status: a === b,
+                      msg: a + " === " + b + ": " + msg, context: context});
+}
+
+addEventListener('message', function workerWrapperOnMessage(e) {
+  removeEventListener('message', workerWrapperOnMessage);
+  var data = e.data;
+
+  function loadTest() {
+    var done = function() {
+      client.postMessage({ type: 'finish', context: context });
+    }
+
+    try {
+      importScripts(data.script);
+      // runTest() is provided by the test.
+      runTest().then(done, done);
+    } catch(e) {
+      client.postMessage({
+        type: 'status',
+        status: false,
+        msg: 'worker failed to import ' + data.script + "; error: " + e.message,
+        context: context
+      });
+      done();
+    }
+  }
+
+  if ("ServiceWorker" in self) {
+    self.clients.matchAll().then(function(clients) {
+      for (var i = 0; i < clients.length; ++i) {
+        if (clients[i].url.indexOf("message_receiver.html") > -1) {
+          client = clients[i];
+          break;
+        }
+      }
+      if (!client) {
+        dump("We couldn't find the message_receiver window, the test will fail\n");
+      }
+      context = "ServiceWorker";
+      loadTest();
+    });
+  } else {
+    client = self;
+    context = "Worker";
+    loadTest();
+  }
+});
-- 
cgit v1.2.3