From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../mochitest/fetch/common_readableStreams.js | 414 +++++ .../mochitest/fetch/common_temporaryFileBlob.js | 146 ++ dom/tests/mochitest/fetch/empty.js | 0 dom/tests/mochitest/fetch/empty.js^headers^ | 1 + dom/tests/mochitest/fetch/fetch_test_framework.js | 165 ++ .../fetch/file_fetch_cached_redirect.html | 1 + .../fetch/file_fetch_cached_redirect.html^headers^ | 3 + .../fetch/file_fetch_csp_block_frame.html | 13 + .../fetch/file_fetch_csp_block_frame.html^headers^ | 2 + dom/tests/mochitest/fetch/file_fetch_observer.html | 146 ++ .../mochitest/fetch/iframe_readableStreams.html | 4 + dom/tests/mochitest/fetch/message_receiver.html | 6 + dom/tests/mochitest/fetch/mochitest.ini | 92 + dom/tests/mochitest/fetch/nested_worker_wrapper.js | 32 + dom/tests/mochitest/fetch/reroute.html | 18 + dom/tests/mochitest/fetch/reroute.js | 27 + dom/tests/mochitest/fetch/reroute.js^headers^ | 1 + dom/tests/mochitest/fetch/slow.sjs | 15 + dom/tests/mochitest/fetch/sw_reroute.js | 43 + dom/tests/mochitest/fetch/test_fetch_basic.html | 23 + dom/tests/mochitest/fetch/test_fetch_basic.js | 179 ++ .../mochitest/fetch/test_fetch_basic_http.html | 23 + dom/tests/mochitest/fetch/test_fetch_basic_http.js | 268 +++ .../test_fetch_basic_http_sw_empty_reroute.html | 23 + .../fetch/test_fetch_basic_http_sw_reroute.html | 23 + .../fetch/test_fetch_basic_sw_empty_reroute.html | 23 + .../fetch/test_fetch_basic_sw_reroute.html | 23 + .../fetch/test_fetch_cached_redirect.html | 22 + .../mochitest/fetch/test_fetch_cached_redirect.js | 17 + dom/tests/mochitest/fetch/test_fetch_cors.html | 23 + dom/tests/mochitest/fetch/test_fetch_cors.js | 1883 ++++++++++++++++++++ .../fetch/test_fetch_cors_sw_empty_reroute.html | 23 + .../fetch/test_fetch_cors_sw_reroute.html | 23 + .../mochitest/fetch/test_fetch_csp_block.html | 50 + dom/tests/mochitest/fetch/test_fetch_observer.html | 40 + .../fetch/test_fetch_user_control_rp.html | 103 ++ .../mochitest/fetch/test_formdataparsing.html | 23 + dom/tests/mochitest/fetch/test_formdataparsing.js | 368 ++++ .../fetch/test_formdataparsing_sw_reroute.html | 23 + dom/tests/mochitest/fetch/test_headers.html | 17 + dom/tests/mochitest/fetch/test_headers_common.js | 327 ++++ .../mochitest/fetch/test_headers_mainthread.html | 155 ++ .../mochitest/fetch/test_headers_sw_reroute.html | 17 + .../mochitest/fetch/test_readableStreams.html | 86 + dom/tests/mochitest/fetch/test_request.html | 23 + dom/tests/mochitest/fetch/test_request.js | 744 ++++++++ .../mochitest/fetch/test_request_context.html | 19 + .../mochitest/fetch/test_request_sw_reroute.html | 23 + dom/tests/mochitest/fetch/test_response.html | 23 + dom/tests/mochitest/fetch/test_response.js | 346 ++++ .../mochitest/fetch/test_responseReadyForWasm.html | 44 + .../mochitest/fetch/test_response_sw_reroute.html | 23 + .../mochitest/fetch/test_temporaryFileBlob.html | 41 + .../fetch/test_webassembly_streaming.html | 22 + dom/tests/mochitest/fetch/utils.js | 51 + .../mochitest/fetch/worker_readableStreams.js | 26 + .../mochitest/fetch/worker_temporaryFileBlob.js | 31 + dom/tests/mochitest/fetch/worker_wrapper.js | 85 + 58 files changed, 6415 insertions(+) create mode 100644 dom/tests/mochitest/fetch/common_readableStreams.js 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/file_fetch_cached_redirect.html create mode 100644 dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ create mode 100644 dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html create mode 100644 dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ create mode 100644 dom/tests/mochitest/fetch/file_fetch_observer.html create mode 100644 dom/tests/mochitest/fetch/iframe_readableStreams.html 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/slow.sjs 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_cached_redirect.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_cached_redirect.js 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_fetch_csp_block.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_observer.html create mode 100644 dom/tests/mochitest/fetch/test_fetch_user_control_rp.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_readableStreams.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_responseReadyForWasm.html 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/test_webassembly_streaming.html create mode 100644 dom/tests/mochitest/fetch/utils.js create mode 100644 dom/tests/mochitest/fetch/worker_readableStreams.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_readableStreams.js b/dom/tests/mochitest/fetch/common_readableStreams.js new file mode 100644 index 0000000000..a739e1dbfa --- /dev/null +++ b/dom/tests/mochitest/fetch/common_readableStreams.js @@ -0,0 +1,414 @@ +const SAME_COMPARTMENT = "same-compartment"; +const IFRAME_COMPARTMENT = "iframe-compartment"; +const BIG_BUFFER_SIZE = 1000000; +const ITER_MAX = 10; + +function makeBuffer(size) { + let buffer = new Uint8Array(size); + buffer.fill(42); + + let value = 0; + for (let i = 0; i < 1000000; i += 1000) { + buffer.set([++value % 255], i); + } + + return buffer; +} + +function apply_compartment(compartment, data) { + if (compartment == SAME_COMPARTMENT) { + return self[data.func](data.args, self); + } + + if (compartment == IFRAME_COMPARTMENT) { + const iframe = document.querySelector("#iframe").contentWindow; + return iframe[data.func](data.args, self); + } + + ok(false, "Invalid compartment value"); +} + +async function test_nativeStream(compartment) { + info("test_nativeStream"); + + let r = await fetch("/"); + + return apply_compartment(compartment, { + func: "test_nativeStream_continue", + args: r, + }); +} + +async function test_nativeStream_continue(r, that) { + that.ok(r.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let a = r.clone(); + that.ok(a instanceof that.Response, "We have a cloned Response"); + that.ok(a.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let b = a.clone(); + that.ok(b instanceof that.Response, "We have a cloned Response"); + that.ok(b.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let blob = await r.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); + let d = await a.body.getReader().read(); + + that.ok(!d.done, "We have read something!"); + blob = await b.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); +} + +async function test_timeout(compartment) { + info("test_timeout"); + + let blob = new Blob([""]); + let r = await fetch(URL.createObjectURL(blob)); + + return apply_compartment(compartment, { + func: "test_timeout_continue", + args: r, + }); +} + +async function test_timeout_continue(r, that) { + await r.body.getReader().read(); + + await new Promise(resolve => setTimeout(resolve, 0)); + + try { + await r.blob(); + that.ok(false, "We cannot have a blob here!"); + } catch (exc) { + that.ok(true, "We cannot have a blob here!"); + } +} + +async function test_nonNativeStream(compartment) { + info("test_nonNativeStream"); + + let buffer = makeBuffer(BIG_BUFFER_SIZE); + info("Buffer size: " + buffer.byteLength); + + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue(buffer); + controller.close(); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_nonNativeStream_continue", + args: { r, buffer }, + }); +} + +async function test_nonNativeStream_continue(data, that) { + that.ok( + data.r.body instanceof that.ReadableStream, + "We have a ReadableStream" + ); + + let a = data.r.clone(); + that.ok(a instanceof that.Response, "We have a cloned Response"); + that.ok(a.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let b = a.clone(); + that.ok(b instanceof that.Response, "We have a cloned Response"); + that.ok(b.body instanceof that.ReadableStream, "We have a ReadableStream"); + + let blob = await data.r.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); + let d = await a.body.getReader().read(); + + that.ok(!d.done, "We have read something!"); + blob = await b.blob(); + + that.ok(blob instanceof that.Blob, "We have a blob"); + that.is(blob.size, data.buffer.byteLength, "Blob size matches"); +} + +async function test_noUint8Array(compartment) { + info("test_noUint8Array"); + + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue("hello world!"); + controller.close(); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_noUint8Array_continue", + args: r, + }); +} + +async function test_noUint8Array_continue(r, that) { + that.ok(r.body instanceof that.ReadableStream, "We have a ReadableStream"); + + try { + await r.blob(); + that.ok(false, "We cannot have a blob here!"); + } catch (ex) { + that.ok(true, "We cannot have a blob here!"); + } +} + +async function test_pendingStream(compartment) { + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue(makeBuffer(BIG_BUFFER_SIZE)); + // Let's keep this controler open. + self.ccc = controller; + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_pendingStream_continue", + args: r, + }); +} + +async function test_pendingStream_continue(r, that) { + let d = await r.body.getReader().read(); + + that.ok(!d.done, "We have read something!"); + + if ("close" in that) { + that.close(); + } +} + +async function test_nativeStream_cache(compartment) { + info("test_nativeStream_cache"); + + let origBody = "123456789abcdef"; + let url = "/nativeStream"; + + let cache = await caches.open("nativeStream"); + + info("Storing a body as a string"); + await cache.put(url, new Response(origBody)); + + return apply_compartment(compartment, { + func: "test_nativeStream_cache_continue", + args: { caches, cache, url, origBody }, + }); +} + +async function test_nativeStream_cache_continue(data, that) { + that.info("Retrieving the stored value"); + let cacheResponse = await data.cache.match(data.url); + + that.info("Converting the response to text"); + let cacheBody = await cacheResponse.text(); + + that.is(data.origBody, cacheBody, "Bodies match"); + + await data.caches.delete("nativeStream"); +} + +async function test_nonNativeStream_cache(compartment) { + info("test_nonNativeStream_cache"); + + let url = "/nonNativeStream"; + + let cache = await caches.open("nonNativeStream"); + let buffer = makeBuffer(BIG_BUFFER_SIZE); + info("Buffer size: " + buffer.byteLength); + + info("Storing a body as a string"); + let r = new Response( + new ReadableStream({ + start: controller => { + controller.enqueue(buffer); + controller.close(); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_nonNativeStream_cache_continue", + args: { caches, cache, buffer, r }, + }); +} + +async function test_nonNativeStream_cache_continue(data, that) { + await data.cache.put(data.url, data.r); + + that.info("Retrieving the stored value"); + let cacheResponse = await data.cache.match(data.url); + + that.info("Converting the response to text"); + let cacheBody = await cacheResponse.arrayBuffer(); + + that.ok(cacheBody instanceof that.ArrayBuffer, "Body is an array buffer"); + that.is(cacheBody.byteLength, BIG_BUFFER_SIZE, "Body length is correct"); + + let value = 0; + for (let i = 0; i < 1000000; i += 1000) { + that.is( + new Uint8Array(cacheBody)[i], + ++value % 255, + "byte in position " + i + " is correct" + ); + } + + await data.caches.delete("nonNativeStream"); +} + +async function test_codeExecution(compartment) { + info("test_codeExecution"); + + let r = new Response( + new ReadableStream({ + start(c) { + controller = c; + }, + pull() { + console.log("pull called"); + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_codeExecution_continue", + args: r, + }); +} + +// This is intended to just be a drop-in replacement for an old observer +// notification. +function addConsoleStorageListener(listener) { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + listener.__handler = (message, id) => { + listener.observe(message, id); + }; + ConsoleAPIStorage.addLogEventListener( + listener.__handler, + SpecialPowers.wrap(document).nodePrincipal + ); +} + +function removeConsoleStorageListener(listener) { + const ConsoleAPIStorage = SpecialPowers.Cc[ + "@mozilla.org/consoleAPI-storage;1" + ].getService(SpecialPowers.Ci.nsIConsoleAPIStorage); + ConsoleAPIStorage.removeLogEventListener(listener.__handler); +} + +async function test_codeExecution_continue(r, that) { + function consoleListener() { + addConsoleStorageListener(this); + } + + var promise = new Promise(resolve => { + consoleListener.prototype = { + observe(aSubject) { + that.ok(true, "Something has been received"); + + var obj = aSubject.wrappedJSObject; + if (obj.arguments[0] && obj.arguments[0] === "pull called") { + that.ok(true, "Message received!"); + removeConsoleStorageListener(this); + resolve(); + } + }, + }; + }); + + var cl = new consoleListener(); + + r.body.getReader().read(); + await promise; +} + +async function test_global(compartment) { + info("test_global: " + compartment); + + self.foo = 42; + self.iter = ITER_MAX; + + let r = new Response( + new ReadableStream({ + start(c) { + self.controller = c; + }, + pull() { + if (!("iter" in self) || self.iter < 0 || self.iter > ITER_MAX) { + throw "Something bad is happening here!"; + } + + let buffer = new Uint8Array(1); + buffer.fill(self.foo); + self.controller.enqueue(buffer); + + if (--self.iter == 0) { + controller.close(); + } + }, + }) + ); + + return apply_compartment(compartment, { + func: "test_global_continue", + args: r, + }); +} + +async function test_global_continue(r, that) { + let a = await r.arrayBuffer(); + + that.is( + Object.getPrototypeOf(a), + that.ArrayBuffer.prototype, + "Body is an array buffer" + ); + that.is(a.byteLength, ITER_MAX, "Body length is correct"); + + for (let i = 0; i < ITER_MAX; ++i) { + that.is(new Uint8Array(a)[i], 42, "Byte " + i + " is correct"); + } +} + +function workify(func) { + info("Workifying " + func); + + return new Promise((resolve, reject) => { + let worker = new Worker("worker_readableStreams.js"); + worker.postMessage(func); + worker.onmessage = function (e) { + if (e.data.type == "done") { + resolve(); + return; + } + + if (e.data.type == "error") { + reject(e.data.message); + return; + } + + if (e.data.type == "test") { + ok(e.data.test, e.data.message); + return; + } + + if (e.data.type == "info") { + info(e.data.message); + return; + } + }; + }); +} diff --git a/dom/tests/mochitest/fetch/common_temporaryFileBlob.js b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js new file mode 100644 index 0000000000..51aed67cf2 --- /dev/null +++ b/dom/tests/mochitest/fetch/common_temporaryFileBlob.js @@ -0,0 +1,146 @@ +var data = new Array(256).join("1234567890ABCDEF"); + +function test_fetch_basic() { + info("Simple fetch 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"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }); +} + +function test_fetch_worker() { + generic_worker_test("fetch in workers", "fetch"); +} + +function test_xhr_basic() { + info("Simple XHR test"); + + let xhr = new XMLHttpRequest(); + xhr.responseType = "blob"; + xhr.open("POST", "/tests/dom/xhr/tests/temporaryFileBlob.sjs"); + xhr.send(data); + + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + let blob = xhr.response; + + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + } + }; +} + +function test_xhr_worker() { + generic_worker_test("XHR in workers", "xhr"); +} + +function test_response_basic() { + info("Response"); + + let r = new Response(data); + r.blob().then(blob => { + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }); +} + +function test_response_worker() { + generic_worker_test("Response in workers", "response"); +} + +function test_request_basic() { + info("Request"); + + let r = new Request("https://example.com", { body: data, method: "POST" }); + r.blob().then(blob => { + ok(blob instanceof Blob, "We have a blob!"); + is(blob.size, data.length, "Data length matches"); + if ("SpecialPowers" in self) { + is( + SpecialPowers.wrap(blob).blobImplType, + "StreamBlobImpl[TemporaryFileBlobImpl]", + "We have a blob stored into a stream file" + ); + } + + var fr = new FileReader(); + fr.readAsText(blob); + fr.onload = function () { + is(fr.result, data, "Data content matches"); + next(); + }; + }); +} + +function test_request_worker() { + generic_worker_test("Request in workers", "request"); +} + +function generic_worker_test(title, what) { + info(title); + + 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(what); +} diff --git a/dom/tests/mochitest/fetch/empty.js b/dom/tests/mochitest/fetch/empty.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/dom/tests/mochitest/fetch/empty.js^headers^ b/dom/tests/mochitest/fetch/empty.js^headers^ new file mode 100644 index 0000000000..d0b9633bb0 --- /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 0000000000..a985c43b64 --- /dev/null +++ b/dom/tests/mochitest/fetch/fetch_test_framework.js @@ -0,0 +1,165 @@ +function testScript(script) { + function makeWrapperUrl(wrapper) { + return wrapper + "?script=" + script; + } + let workerWrapperUrl = makeWrapperUrl("worker_wrapper.js"); + + // 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(4); + + // 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.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.idle_timeout", 60000], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ], + }, + resolve + ); + }); + } + + function workerTest() { + return new Promise(function (resolve, reject) { + var worker = new Worker(workerWrapperUrl); + 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 }); + }); + } + + function nestedWorkerTest() { + return new Promise(function (resolve, reject) { + var worker = new Worker(makeWrapperUrl("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 }); + }); + } + + 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.installing || + registration.waiting || + registration.active; + var iframe; + + window.addEventListener("message", function onMessage(event) { + if (event.data.context != "ServiceWorker") { + return; + } + if (event.data.type == "finish") { + window.removeEventListener("message", onMessage); + iframe.remove(); + registration.unregister().then(resolve).catch(reject); + } else if (event.data.type == "status") { + ok(event.data.status, event.data.context + ": " + event.data.msg); + } + }); + + worker.onerror = reject; + + iframe = document.createElement("iframe"); + iframe.src = "message_receiver.html"; + iframe.onload = function () { + worker.postMessage({ script }); + }; + document.body.appendChild(iframe); + } + + navigator.serviceWorker + .register(workerWrapperUrl, { scope: "." }) + .then(setupSW); + }); + } + + 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 () { + return 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 () { + try { + if (parent && parent.finishTest) { + parent.finishTest(); + return; + } + } catch {} + SimpleTest.finish(); + }); +} diff --git a/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html new file mode 100644 index 0000000000..64e3289892 --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html @@ -0,0 +1 @@ +My contents don't matter. Only my header matters! diff --git a/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ new file mode 100644 index 0000000000..eee464d0eb --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^ @@ -0,0 +1,3 @@ +HTTP 302 Redirect +Location: //example.org/target_does_not_matter.html +Cache-Control: max-age=10 diff --git a/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html new file mode 100644 index 0000000000..793575f45c --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html @@ -0,0 +1,13 @@ + + + + diff --git a/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ new file mode 100644 index 0000000000..4c43573eb7 --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^ @@ -0,0 +1,2 @@ +Content-Type: text/html +Content-Security-Policy: connect-src 'self' diff --git a/dom/tests/mochitest/fetch/file_fetch_observer.html b/dom/tests/mochitest/fetch/file_fetch_observer.html new file mode 100644 index 0000000000..480198fa6f --- /dev/null +++ b/dom/tests/mochitest/fetch/file_fetch_observer.html @@ -0,0 +1,146 @@ + diff --git a/dom/tests/mochitest/fetch/iframe_readableStreams.html b/dom/tests/mochitest/fetch/iframe_readableStreams.html new file mode 100644 index 0000000000..11a3838789 --- /dev/null +++ b/dom/tests/mochitest/fetch/iframe_readableStreams.html @@ -0,0 +1,4 @@ + + diff --git a/dom/tests/mochitest/fetch/message_receiver.html b/dom/tests/mochitest/fetch/message_receiver.html new file mode 100644 index 0000000000..82cb587c72 --- /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 0000000000..6a7005bd56 --- /dev/null +++ b/dom/tests/mochitest/fetch/mochitest.ini @@ -0,0 +1,92 @@ +[DEFAULT] +tags = condprof +support-files = + fetch_test_framework.js + file_fetch_cached_redirect.html + file_fetch_cached_redirect.html^headers^ + file_fetch_csp_block_frame.html + file_fetch_csp_block_frame.html^headers^ + test_fetch_basic.js + test_fetch_basic_http.js + test_fetch_cached_redirect.js + test_fetch_cors.js + file_fetch_observer.html + 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^ + slow.sjs + sw_reroute.js + empty.js + empty.js^headers^ + worker_temporaryFileBlob.js + common_temporaryFileBlob.js + common_readableStreams.js + worker_readableStreams.js + iframe_readableStreams.html + !/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 + !/dom/security/test/csp/file_redirects_resource.sjs + !/dom/base/test/referrer_helper.js + !/dom/base/test/referrer_testserver.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_cached_redirect.html] +[test_fetch_cors.html] +skip-if = + http3 +[test_fetch_cors_sw_reroute.html] +skip-if = + os == "android" # Bug 1623134 + http3 +[test_fetch_cors_sw_empty_reroute.html] +skip-if = + os == "android" # Bug 1623134 + http3 +[test_fetch_csp_block.html] +[test_fetch_observer.html] +skip-if = + http3 +[test_fetch_user_control_rp.html] +skip-if = + http3 +[test_formdataparsing.html] +[test_formdataparsing_sw_reroute.html] +[test_request.html] +[test_request_context.html] +[test_request_sw_reroute.html] +[test_response.html] +skip-if = + http3 +[test_response_sw_reroute.html] +skip-if = + http3 +[test_temporaryFileBlob.html] +[test_readableStreams.html] +scheme=https +skip-if = + http3 +[test_responseReadyForWasm.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 0000000000..1a14cf06d9 --- /dev/null +++ b/dom/tests/mochitest/fetch/nested_worker_wrapper.js @@ -0,0 +1,32 @@ +function getScriptUrl() { + return new URL(location.href).searchParams.get("script"); +} + +// 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?script=" + getScriptUrl()); + + 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 0000000000..bb12212ea9 --- /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 0000000000..a4f309d780 --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js @@ -0,0 +1,27 @@ +onfetch = function (e) { + if (e.request.url.includes("Referer")) { + // 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=" + JSON.stringify({ Referer: self.location.href }); + + 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, + 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 0000000000..d0b9633bb0 --- /dev/null +++ b/dom/tests/mochitest/fetch/reroute.js^headers^ @@ -0,0 +1 @@ +Service-Worker-Allowed: / diff --git a/dom/tests/mochitest/fetch/slow.sjs b/dom/tests/mochitest/fetch/slow.sjs new file mode 100644 index 0000000000..27b9719b71 --- /dev/null +++ b/dom/tests/mochitest/fetch/slow.sjs @@ -0,0 +1,15 @@ +function handleRequest(request, response) { + response.processAsync(); + + timer = Components.classes["@mozilla.org/timer;1"].createInstance( + Components.interfaces.nsITimer + ); + timer.init( + function () { + response.write("Here the content. But slowly."); + response.finish(); + }, + 1000, + Components.interfaces.nsITimer.TYPE_ONE_SHOT + ); +} diff --git a/dom/tests/mochitest/fetch/sw_reroute.js b/dom/tests/mochitest/fetch/sw_reroute.js new file mode 100644 index 0000000000..73f4aecae6 --- /dev/null +++ b/dom/tests/mochitest/fetch/sw_reroute.js @@ -0,0 +1,43 @@ +var gRegistration; +var iframe; + +function testScript(script) { + var scope = "./reroute.html?" + script.replace(".js", ""); + function setupSW(registration) { + gRegistration = registration; + + iframe = document.createElement("iframe"); + iframe.src = scope; + document.body.appendChild(iframe); + } + + SpecialPowers.pushPrefEnv( + { + set: [ + ["dom.serviceWorkers.enabled", true], + ["dom.serviceWorkers.testing.enabled", true], + ["dom.serviceWorkers.exemptFromPerDomainMax", true], + ["dom.serviceWorkers.idle_timeout", 60000], + ], + }, + function () { + var scriptURL = location.href.includes("sw_empty_reroute.html") + ? "empty.js" + : "reroute.js"; + navigator.serviceWorker + .register(scriptURL, { scope }) + .then(swr => waitForState(swr.installing, "activated", swr)) + .then(setupSW); + } + ); +} + +function finishTest() { + iframe.remove(); + 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 0000000000..7f3536c92e --- /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 0000000000..27343d8662
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic.js
@@ -0,0 +1,179 @@
+function testAboutURL() {
+  var p1 = fetch("about:blank").then(
+    function (res) {
+      ok(false, "about:blank should fail");
+    },
+    function (e) {
+      ok(e instanceof TypeError, "about:blank should fail");
+    }
+  );
+
+  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 });
+      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 testMozErrors() {
+  // mozErrors shouldn't be available to content and be ignored.
+  return fetch("http://localhost:4/should/fail", { mozErrors: true })
+    .then(res => {
+      ok(false, "Request should not succeed");
+    })
+    .catch(err => {
+      ok(err instanceof TypeError);
+    });
+}
+
+function testRequestMozErrors() {
+  // mozErrors shouldn't be available to content and be ignored.
+  const r = new Request("http://localhost:4/should/fail", { mozErrors: true });
+  return fetch(r)
+    .then(res => {
+      ok(false, "Request should not succeed");
+    })
+    .catch(err => {
+      ok(err instanceof TypeError);
+    });
+}
+
+function testViewSourceURL() {
+  var p2 = fetch("view-source:/").then(
+    function (res) {
+      ok(false, "view-source: URL should fail");
+    },
+    function (e) {
+      ok(e instanceof TypeError, "view-source: URL should fail");
+    }
+  );
+}
+
+function runTest() {
+  return Promise.resolve()
+    .then(testAboutURL)
+    .then(testDataURL)
+    .then(testSameOriginBlobURL)
+    .then(testNonGetBlobURL)
+    .then(testMozErrors)
+    .then(testRequestMozErrors)
+    .then(testViewSourceURL);
+  // 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 0000000000..f6916501d7
--- /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 0000000000..781af2ecde
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http.js
@@ -0,0 +1,268 @@
+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]
+      );
+      if (entry[0] != "file_XHR_pass3.txt") {
+        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]
+      );
+      if (entry[0] != "file_XHR_pass3.txt") {
+        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 => {
+    is(r.status, 200, "status should match");
+    return r.blob().then(b => {
+      is(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 => {
+    is(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 0000000000..5ea6a6227c
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..5ea6a6227c
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..c5cb02571a
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..c5cb02571a
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Bug 1039846 - Test fetch() function in worker
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_cached_redirect.html b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.html
new file mode 100644
index 0000000000..d172957bab
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Bug 1374943 - Test fetch cached redirects
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_cached_redirect.js b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.js
new file mode 100644
index 0000000000..48d9b2231f
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cached_redirect.js
@@ -0,0 +1,17 @@
+async function testCachedRedirectErrorMode() {
+  // This is a file that returns a 302 to someplace else and will be cached.
+  const REDIRECTING_URL = "file_fetch_cached_redirect.html";
+
+  let firstResponse = await fetch(REDIRECTING_URL, { redirect: "manual" });
+  // okay, now it should be in the cahce.
+  try {
+    let secondResponse = await fetch(REDIRECTING_URL, { redirect: "error" });
+  } catch (ex) {}
+
+  ok(true, "didn't crash");
+}
+
+function runTest() {
+  return Promise.resolve().then(testCachedRedirectErrorMode);
+  // Put more promise based tests here.
+}
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 0000000000..b079df8cba
--- /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 0000000000..05ce221435
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors.js
@@ -0,0 +1,1883 @@
+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 "same-origin".
+      pass: 1,
+      cookie: cookieStr,
+    },
+    {
+      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 " + JSON.stringify(test));
+    is(res.status, 200, "wrong status in test for " + JSON.stringify(test));
+    is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test));
+    return res.text().then(function (v) {
+      is(
+        v,
+        "hello pass\n",
+        "wrong text in test for " + JSON.stringify(test)
+      );
+    });
+  }
+
+  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 " + JSON.stringify(test));
+        ok(e instanceof TypeError, "Test should fail " + JSON.stringify(test));
+        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
+    { pass: 0, 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",
+            ].includes(test.headers[name].toLowerCase()))
+        );
+      }
+      req.url += "&headers=" + escape(JSON.stringify(test.headers));
+      reqHeaders = escape(
+        Object.keys(test.headers)
+          .filter(isUnsafeHeader)
+          .map(s => s.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(JSON.stringify(test.responseHeaders));
+    }
+    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 " + JSON.stringify(test));
+            if (test.status) {
+              is(
+                res.status,
+                test.status,
+                "wrong status in test for " + JSON.stringify(test)
+              );
+              is(
+                res.statusText,
+                test.statusMessage,
+                "wrong status text for " + JSON.stringify(test)
+              );
+            } else {
+              is(
+                res.status,
+                200,
+                "wrong status in test for " + JSON.stringify(test)
+              );
+              is(
+                res.statusText,
+                "OK",
+                "wrong status text for " + JSON.stringify(test)
+              );
+            }
+            if (test.responseHeaders) {
+              for (header in test.responseHeaders) {
+                if (!test.expectedResponseHeaders.includes(header)) {
+                  is(
+                    res.headers.has(header),
+                    false,
+                    "|Headers.has()|wrong response header (" +
+                      header +
+                      ") in test for " +
+                      JSON.stringify(test)
+                  );
+                } else {
+                  is(
+                    res.headers.get(header),
+                    test.responseHeaders[header],
+                    "|Headers.get()|wrong response header (" +
+                      header +
+                      ") in test for " +
+                      JSON.stringify(test)
+                  );
+                }
+              }
+            }
+
+            return res.text();
+          })
+          .then(function (v) {
+            if (test.method !== "HEAD") {
+              is(
+                v,
+                "hello pass\n",
+                "wrong responseText in test for " + JSON.stringify(test)
+              );
+            } else {
+              is(
+                v,
+                "",
+                "wrong responseText in HEAD test for " + JSON.stringify(test)
+              );
+            }
+          })
+          .catch(function (e) {
+            ok(!test.pass, "Expected test failure for " + JSON.stringify(test));
+            ok(
+              e instanceof TypeError,
+              "Exception should be TypeError for " + JSON.stringify(test)
+            );
+          });
+      })(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(JSON.stringify(test.hops));
+    } else {
+      url = baseURL + "allowOrigin=" + escape(test.origin || origin);
+    }
+    req = {
+      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 " + JSON.stringify(test));
+    is(res.status, 200, "wrong status in test for " + JSON.stringify(test));
+    is(res.statusText, "OK", "wrong status text for " + JSON.stringify(test));
+    return res.text().then(function (v) {
+      is(
+        v,
+        "hello pass\n",
+        "wrong text in test for " + JSON.stringify(test)
+      );
+    });
+  }
+
+  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 " + JSON.stringify(test));
+        ok(
+          e instanceof TypeError,
+          "Exception should be TypeError for " + JSON.stringify(test)
+        );
+        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 " + JSON.stringify(test));
+
+    // 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 " + JSON.stringify(test)
+    );
+
+    return unfiltered.text().then(function (v) {
+      if (test.status === 200) {
+        const expected =
+          SpecialPowers.getIntPref(
+            "browser.opaqueResponseBlocking.filterFetchResponse"
+          ) > 0
+            ? ""
+            : "hello pass\n";
+        is(v, expected, "wrong text in test for " + JSON.stringify(test));
+      }
+    });
+  }
+
+  function runATest(tests, i) {
+    if (typeof SpecialPowers !== "object") {
+      finalPromiseResolve();
+      return;
+    }
+
+    var test = tests[i];
+    var request = makeRequest(test);
+    fetch(request).then(
+      function (res) {
+        ok(test.pass, "Expected test to pass " + JSON.stringify(test));
+        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 " + JSON.stringify(test));
+        ok(e instanceof TypeError, "Test should fail " + JSON.stringify(test));
+        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(JSON.stringify(test.hops)),
+      method: test.method,
+      headers: test.headers,
+      body: test.body,
+    };
+
+    if (test.headers) {
+      req.url += "&headers=" + escape(JSON.stringify(test.headers));
+    }
+
+    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 " + JSON.stringify(test));
+            is(
+              res.status,
+              200,
+              "wrong status in test for " + JSON.stringify(test)
+            );
+            is(
+              res.statusText,
+              "OK",
+              "wrong status text for " + JSON.stringify(test)
+            );
+            is(
+              res.type,
+              "cors",
+              "wrong response type for " + JSON.stringify(test)
+            );
+            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 " + JSON.stringify(test)
+              );
+            });
+          },
+          function (e) {
+            ok(!test.pass, "Expected test failure for " + JSON.stringify(test));
+            ok(
+              e instanceof TypeError,
+              "Exception should be TypeError for " + JSON.stringify(test)
+            );
+          }
+        );
+      })(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(JSON.stringify(test.hops)),
+      method: test.method,
+      headers: test.headers,
+      body: test.body,
+    };
+
+    if (test.headers) {
+      req.url += "&headers=" + escape(JSON.stringify(test.headers));
+    }
+
+    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 " + JSON.stringify(test)
+              );
+              // 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 " + JSON.stringify(test)
+              );
+              is(
+                res.status,
+                0,
+                "wrong status in test for " + JSON.stringify(test)
+              );
+              is(
+                res.statusText,
+                "",
+                "wrong status text for " + JSON.stringify(test)
+              );
+              is(res.url, "", "wrong response url for " + JSON.stringify(test));
+              return res.text().then(function (v) {
+                is(
+                  v,
+                  "",
+                  "wrong responseText in test for " + JSON.stringify(test)
+                );
+              });
+            },
+            function (e) {
+              ok(
+                !test.pass,
+                "Expected test failure for " + JSON.stringify(test)
+              );
+              ok(
+                e instanceof TypeError,
+                "Exception should be TypeError for " + JSON.stringify(test)
+              );
+            }
+          );
+      })(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=" + encodeURIComponent(JSON.stringify(dict))
+  ).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();
+  let promise = Promise.resolve();
+  if (typeof SpecialPowers === "object") {
+    promise = SpecialPowers.pushPrefEnv({
+      // Bug 1617611: Fix all the tests broken by "cookies SameSite=lax by default"
+      set: [["network.cookie.sameSite.laxByDefault", false]],
+    });
+  }
+
+  return promise
+    .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 0000000000..ab519de573
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..ab519de573
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  Bug 1039846 - Test fetch() CORS mode
+  
+  
+
+
+

+ +

+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_csp_block.html b/dom/tests/mochitest/fetch/test_fetch_csp_block.html
new file mode 100644
index 0000000000..0347c7319e
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_csp_block.html
@@ -0,0 +1,50 @@
+
+
+
+  
+  Test fetch() rejects when CSP blocks
+  
+  
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_observer.html b/dom/tests/mochitest/fetch/test_fetch_observer.html
new file mode 100644
index 0000000000..7d9c80f4c8
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_observer.html
@@ -0,0 +1,40 @@
+
+
+
+
+  Test FetchObserver
+  
+  
+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_fetch_user_control_rp.html b/dom/tests/mochitest/fetch/test_fetch_user_control_rp.html
new file mode 100644
index 0000000000..67ad489191
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_fetch_user_control_rp.html
@@ -0,0 +1,103 @@
+
+
+
+  
+  Test fetch user control referrer policy Bug 1304623
+  
+  
+    
+  
+
+
+
+  
+
+
diff --git a/dom/tests/mochitest/fetch/test_formdataparsing.html b/dom/tests/mochitest/fetch/test_formdataparsing.html
new file mode 100644
index 0000000000..d82f9425ad
--- /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 0000000000..5792ddfe01
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_formdataparsing.js
@@ -0,0 +1,368 @@
+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,
+    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 0000000000..0e25c404a9
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..b9cad02c2f
--- /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 0000000000..3b2604f88f
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_headers_common.js
@@ -0,0 +1,327 @@
+//
+// 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");
+
+  const value_bam = "boom";
+  let testHTTPWhitespace = ["\t", "\n", "\r", " "];
+  while (testHTTPWhitespace.length) {
+    headers.delete(name);
+
+    let char = testHTTPWhitespace.shift();
+    headers.set(name, char);
+    checkGet(
+      headers,
+      name,
+      "",
+      "Header value " +
+        char +
+        " should be normalized before checking and throwing"
+    );
+    headers.delete(name);
+
+    let valueFront = char + value_bam;
+    headers.set(name, valueFront);
+    checkGet(
+      headers,
+      name,
+      value_bam,
+      "Header value " +
+        valueFront +
+        " should be normalized before checking and throwing"
+    );
+
+    headers.delete(name);
+
+    let valueBack = value_bam + char;
+    headers.set(name, valueBack);
+    checkGet(
+      headers,
+      name,
+      value_bam,
+      "Header value " +
+        valueBack +
+        " should be normalized before checking and throwing"
+    );
+  }
+
+  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, "ba\nm");
+    },
+    TypeError,
+    "Append invalid header value should throw TypeError."
+  );
+
+  shouldThrow(
+    function () {
+      headers.append(name, "ba\rm");
+    },
+    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();
+  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", "foo2"], "Correct key iterator");
+  arrayEquals(
+    iterate(value_iter),
+    ["bar, " + ehsanInflated, "baz2"],
+    "Correct value iterator"
+  );
+  arrayEquals(
+    iterate(entries_iter),
+    [
+      ["foo", "bar, " + ehsanInflated],
+      ["foo2", "baz2"],
+    ],
+    "Correct entries iterator"
+  );
+
+  arrayEquals(
+    iterateForOf(headers),
+    [
+      ["foo", "bar, " + ehsanInflated],
+      ["foo2", "baz2"],
+    ],
+    "Correct entries iterator"
+  );
+  arrayEquals(
+    iterateForOf(new Headers(headers)),
+    [
+      ["foo", "bar, " + 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 0000000000..de78b364e3
--- /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 0000000000..eb8d1109bd
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_headers_sw_reroute.html
@@ -0,0 +1,17 @@
+
+
+
+
+  Test Fetch Headers - Basic
+  
+  
+
+
+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_readableStreams.html b/dom/tests/mochitest/fetch/test_readableStreams.html
new file mode 100644
index 0000000000..1731aa8bae
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_readableStreams.html
@@ -0,0 +1,86 @@
+
+
+
+  
+  Test for ReadableStreams and Fetch
+  
+  
+  
+
+
+  
+  
+
+
+
diff --git a/dom/tests/mochitest/fetch/test_request.html b/dom/tests/mochitest/fetch/test_request.html
new file mode 100644
index 0000000000..3a35665eb5
--- /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 0000000000..7abd833869
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_request.js
@@ -0,0 +1,744 @@
+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.destination, "", "Default destination is the empty string.");
+  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,
+    "same-origin",
+    "Default Request credentials is same-origin"
+  );
+  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.destination, "", "Default destination is the empty string.");
+  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,
+    "same-origin",
+    "Default Request credentials is same-origin"
+  );
+  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 });
+  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 });
+  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");
+    is(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,
+    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 });
+      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 0000000000..db8b8bdc6e
--- /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 0000000000..0250148f1b
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_request_sw_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..af75f957fc
--- /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 0000000000..40a124bfc0
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_response.js
@@ -0,0 +1,346 @@
+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, "", "Default statusText is an empty string");
+}
+
+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");
+  is(res.statusText, "", "Default redirect has status text empty");
+  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 });
+      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 });
+      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_responseReadyForWasm.html b/dom/tests/mochitest/fetch/test_responseReadyForWasm.html
new file mode 100644
index 0000000000..7f312a9fb3
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_responseReadyForWasm.html
@@ -0,0 +1,44 @@
+
+
+
+  
+  Test for Response ready to be used by wasm
+  
+  
+
+
+  
+
+
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 0000000000..b7d52c9849
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_response_sw_reroute.html
@@ -0,0 +1,23 @@
+
+
+
+
+  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 0000000000..8c645e8416
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_temporaryFileBlob.html
@@ -0,0 +1,41 @@
+
+
+
+  
+  Test for Bug 1312410
+  
+  
+  
+
+
+  
+
+
diff --git a/dom/tests/mochitest/fetch/test_webassembly_streaming.html b/dom/tests/mochitest/fetch/test_webassembly_streaming.html
new file mode 100644
index 0000000000..48311ca503
--- /dev/null
+++ b/dom/tests/mochitest/fetch/test_webassembly_streaming.html
@@ -0,0 +1,22 @@
+
+
+
+
+  Test WebAssembly.compileStreaming() and instantiateStreaming()
+  
+  
+
+
+

+ +

+
+
+
+
+
diff --git a/dom/tests/mochitest/fetch/utils.js b/dom/tests/mochitest/fetch/utils.js
new file mode 100644
index 0000000000..8b93c1e90b
--- /dev/null
+++ b/dom/tests/mochitest/fetch/utils.js
@@ -0,0 +1,51 @@
+// 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));
+  }
+}
+
+function waitForState(worker, state, context) {
+  return new Promise(resolve => {
+    if (worker.state === state) {
+      resolve(context);
+      return;
+    }
+    worker.addEventListener("statechange", function onStateChange() {
+      if (worker.state === state) {
+        worker.removeEventListener("statechange", onStateChange);
+        resolve(context);
+      }
+    });
+  });
+}
diff --git a/dom/tests/mochitest/fetch/worker_readableStreams.js b/dom/tests/mochitest/fetch/worker_readableStreams.js
new file mode 100644
index 0000000000..905c73b7fa
--- /dev/null
+++ b/dom/tests/mochitest/fetch/worker_readableStreams.js
@@ -0,0 +1,26 @@
+importScripts("common_readableStreams.js");
+
+function info(message) {
+  postMessage({ type: "info", message });
+}
+
+function ok(a, message) {
+  postMessage({ type: "test", test: !!a, message });
+}
+
+function is(a, b, message) {
+  ok(a === b, message);
+}
+
+onmessage = function (e) {
+  self[e.data](SAME_COMPARTMENT).then(
+    ok => {
+      postMessage({ type: "done" });
+    },
+    exc => {
+      dump(exc);
+      dump(exc.stack);
+      postMessage({ type: "error", message: exc.toString() });
+    }
+  );
+};
diff --git a/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js
new file mode 100644
index 0000000000..e36c32a788
--- /dev/null
+++ b/dom/tests/mochitest/fetch/worker_temporaryFileBlob.js
@@ -0,0 +1,31 @@
+importScripts("common_temporaryFileBlob.js");
+
+function info(msg) {
+  postMessage({ type: "info", msg });
+}
+
+function ok(a, msg) {
+  postMessage({ type: "check", what: !!a, msg });
+}
+
+function is(a, b, msg) {
+  ok(a === b, msg);
+}
+
+function next() {
+  postMessage({ type: "finish" });
+}
+
+onmessage = function (e) {
+  if (e.data == "xhr") {
+    test_xhr_basic();
+  } else if (e.data == "fetch") {
+    test_fetch_basic();
+  } else if (e.data == "response") {
+    test_response_basic();
+  } else if (e.data == "request") {
+    test_request_basic();
+  } else {
+    ok(false, "Unknown message");
+  }
+};
diff --git a/dom/tests/mochitest/fetch/worker_wrapper.js b/dom/tests/mochitest/fetch/worker_wrapper.js
new file mode 100644
index 0000000000..72d00db0e1
--- /dev/null
+++ b/dom/tests/mochitest/fetch/worker_wrapper.js
@@ -0,0 +1,85 @@
+importScripts("utils.js");
+
+function getScriptUrl() {
+  return new URL(location.href).searchParams.get("script");
+}
+
+importScripts(getScriptUrl());
+
+var client;
+var context;
+
+function ok(a, msg) {
+  client.postMessage({
+    type: "status",
+    status: !!a,
+    msg: a + ": " + msg,
+    context,
+  });
+}
+
+function is(a, b, msg) {
+  client.postMessage({
+    type: "status",
+    status: a === b,
+    msg: a + " === " + b + ": " + msg,
+    context,
+  });
+}
+
+addEventListener("message", function workerWrapperOnMessage(e) {
+  removeEventListener("message", workerWrapperOnMessage);
+  var data = e.data;
+
+  function runTestAndReportToClient(event) {
+    var done = function (res) {
+      client.postMessage({ type: "finish", context });
+      return res;
+    };
+
+    try {
+      // runTest() is provided by the test.
+      var result = runTest().then(done, done);
+      if ("waitUntil" in event) {
+        event.waitUntil(result);
+      }
+    } catch (e) {
+      client.postMessage({
+        type: "status",
+        status: false,
+        msg: "worker failed to run " + data.script + "; error: " + e.message,
+        context,
+      });
+      done();
+    }
+  }
+
+  if ("ServiceWorker" in self) {
+    // Fetch requests from a service worker are not intercepted.
+    self.isSWPresent = false;
+
+    e.waitUntil(
+      self.clients
+        .matchAll({ includeUncontrolled: true })
+        .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";
+          runTestAndReportToClient(e);
+        })
+    );
+  } else {
+    client = self;
+    context = "Worker";
+    runTestAndReportToClient(e);
+  }
+});
-- 
cgit v1.2.3