summaryrefslogtreecommitdiffstats
path: root/dom/tests/mochitest/fetch
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /dom/tests/mochitest/fetch
parentInitial commit. (diff)
downloadthunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz
thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--dom/tests/mochitest/fetch/common_readableStreams.js414
-rw-r--r--dom/tests/mochitest/fetch/common_temporaryFileBlob.js146
-rw-r--r--dom/tests/mochitest/fetch/empty.js0
-rw-r--r--dom/tests/mochitest/fetch/empty.js^headers^1
-rw-r--r--dom/tests/mochitest/fetch/fetch_test_framework.js165
-rw-r--r--dom/tests/mochitest/fetch/file_fetch_cached_redirect.html1
-rw-r--r--dom/tests/mochitest/fetch/file_fetch_cached_redirect.html^headers^3
-rw-r--r--dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html13
-rw-r--r--dom/tests/mochitest/fetch/file_fetch_csp_block_frame.html^headers^2
-rw-r--r--dom/tests/mochitest/fetch/file_fetch_observer.html146
-rw-r--r--dom/tests/mochitest/fetch/iframe_readableStreams.html4
-rw-r--r--dom/tests/mochitest/fetch/message_receiver.html6
-rw-r--r--dom/tests/mochitest/fetch/mochitest.ini92
-rw-r--r--dom/tests/mochitest/fetch/nested_worker_wrapper.js32
-rw-r--r--dom/tests/mochitest/fetch/reroute.html18
-rw-r--r--dom/tests/mochitest/fetch/reroute.js27
-rw-r--r--dom/tests/mochitest/fetch/reroute.js^headers^1
-rw-r--r--dom/tests/mochitest/fetch/slow.sjs15
-rw-r--r--dom/tests/mochitest/fetch/sw_reroute.js43
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic.js179
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http.js268
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http_sw_empty_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_http_sw_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_sw_empty_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_basic_sw_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cached_redirect.html22
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cached_redirect.js17
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors.js1883
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors_sw_empty_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_cors_sw_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_csp_block.html50
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_observer.html40
-rw-r--r--dom/tests/mochitest/fetch/test_fetch_user_control_rp.html103
-rw-r--r--dom/tests/mochitest/fetch/test_formdataparsing.html23
-rw-r--r--dom/tests/mochitest/fetch/test_formdataparsing.js368
-rw-r--r--dom/tests/mochitest/fetch/test_formdataparsing_sw_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_headers.html17
-rw-r--r--dom/tests/mochitest/fetch/test_headers_common.js327
-rw-r--r--dom/tests/mochitest/fetch/test_headers_mainthread.html155
-rw-r--r--dom/tests/mochitest/fetch/test_headers_sw_reroute.html17
-rw-r--r--dom/tests/mochitest/fetch/test_readableStreams.html86
-rw-r--r--dom/tests/mochitest/fetch/test_request.html23
-rw-r--r--dom/tests/mochitest/fetch/test_request.js744
-rw-r--r--dom/tests/mochitest/fetch/test_request_context.html19
-rw-r--r--dom/tests/mochitest/fetch/test_request_sw_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_response.html23
-rw-r--r--dom/tests/mochitest/fetch/test_response.js346
-rw-r--r--dom/tests/mochitest/fetch/test_responseReadyForWasm.html44
-rw-r--r--dom/tests/mochitest/fetch/test_response_sw_reroute.html23
-rw-r--r--dom/tests/mochitest/fetch/test_temporaryFileBlob.html41
-rw-r--r--dom/tests/mochitest/fetch/test_webassembly_streaming.html22
-rw-r--r--dom/tests/mochitest/fetch/utils.js51
-rw-r--r--dom/tests/mochitest/fetch/worker_readableStreams.js26
-rw-r--r--dom/tests/mochitest/fetch/worker_temporaryFileBlob.js31
-rw-r--r--dom/tests/mochitest/fetch/worker_wrapper.js85
58 files changed, 6415 insertions, 0 deletions
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
--- /dev/null
+++ b/dom/tests/mochitest/fetch/empty.js
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 @@
+<html><body>My contents don't matter. Only my header matters!</body></html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<script>
+addEventListener('message', evt => {
+ let url = '/tests/dom/security/test/csp/file_redirects_resource.sjs?redir=other&res=xhr-resp';
+ fetch(url).then(response => {
+ parent.postMessage('RESOLVED', '*');
+ }).catch(error => {
+ parent.postMessage('REJECTED', '*');
+ });
+}, { once: true });
+</script>
+</html>
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 @@
+<script>
+function ok(a, msg) {
+ parent.postMessage({ type: "check", status: !!a, message: msg }, "*");
+}
+
+function is(a, b, msg) {
+ ok(a === b, msg);
+}
+
+function testObserver() {
+ ok("FetchObserver" in self, "We have a FetchObserver prototype");
+
+ fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', { observe: o => {
+ ok(!!o, "We have an observer");
+ ok(o instanceof FetchObserver, "The correct object has been passed");
+ is(o.state, "requesting", "By default the state is requesting");
+ next();
+ }});
+}
+
+function testObserveAbort() {
+ var ac = new AbortController();
+
+ fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', {
+ signal: ac.signal,
+ observe: o => {
+ o.onstatechange = () => {
+ ok(true, "StateChange event dispatched");
+ if (o.state == "aborted") {
+ ok(true, "Aborted!");
+ next();
+ }
+ }
+ ac.abort();
+ }
+ });
+}
+
+function testObserveComplete() {
+ var ac = new AbortController();
+
+ fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', {
+ signal: ac.signal,
+ observe: o => {
+ o.onstatechange = () => {
+ ok(true, "StateChange event dispatched");
+ if (o.state == "complete") {
+ ok(true, "Operation completed");
+ next();
+ }
+ }
+ }
+ });
+}
+
+function testObserveErrored() {
+ var ac = new AbortController();
+
+ fetch('foo: bar', {
+ signal: ac.signal,
+ observe: o => {
+ o.onstatechange = () => {
+ ok(true, "StateChange event dispatched");
+ if (o.state == "errored") {
+ ok(true, "Operation completed");
+ next();
+ }
+ }
+ }
+ });
+}
+
+function testObserveResponding() {
+ var ac = new AbortController();
+
+ fetch('http://mochi.test:8888/tests/dom/tests/mochitest/fetch/slow.sjs', {
+ signal: ac.signal,
+ observe: o => {
+ o.onstatechange = () => {
+ if (o.state == "responding") {
+ ok(true, "We have responding events");
+ next();
+ }
+ }
+ }
+ });
+}
+
+function workify(worker) {
+ function methods() {
+ function ok(a, msg) {
+ postMessage( { type: 'check', state: !!a, message: msg });
+ };
+ function is(a, b, msg) {
+ postMessage( { type: 'check', state: a === b, message: msg });
+ };
+ function next() {
+ postMessage( { type: 'finish' });
+ };
+ }
+
+ var str = methods.toString();
+ var methodsContent = str.substring(0, str.length - 1).split('\n').splice(1).join('\n');
+
+ str = worker.toString();
+ var workerContent = str.substring(0, str.length - 1).split('\n').splice(1).join('\n');
+
+ var content = methodsContent + workerContent;
+ var url = URL.createObjectURL(new Blob([content], { type: "application/javascript" }));
+ var w = new Worker(url);
+ w.onmessage = e => {
+ if (e.data.type == 'check') {
+ ok(e.data.state, "WORKER: " + e.data.message);
+ } else if (e.data.type == 'finish') {
+ next();
+ } else {
+ ok(false, "Something went wrong");
+ }
+ }
+}
+
+var steps = [
+ testObserver,
+ testObserveAbort,
+ function() { workify(testObserveAbort); },
+ testObserveComplete,
+ function() { workify(testObserveComplete); },
+ testObserveErrored,
+ function() { workify(testObserveErrored); },
+ testObserveResponding,
+ function() { workify(testObserveResponding); },
+];
+
+function next() {
+ if (!steps.length) {
+ parent.postMessage({ type: "finish" }, "*");
+ return;
+ }
+
+ var step = steps.shift();
+ step();
+}
+
+next();
+
+</script>
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 @@
+<script type="application/javascript" src="common_readableStreams.js"></script>
+<script>
+parent.runTests();
+</script>
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 @@
+<!DOCTYPE html>
+<script>
+ navigator.serviceWorker.onmessage = function(e) {
+ window.parent.postMessage(e.data, "*");
+ };
+</script>
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 @@
+<!DOCTYPE html>
+<script>
+["SimpleTest", "ok", "info", "is", "$"]
+ .forEach((v) => window[v] = window.parent[v]);
+</script>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script>
+// If we are using the empty service worker then requests won't actually
+// get intercepted and response URLs will reflect redirects. This means
+// all our checks should use the "no sw" logic. Otherwise we need to
+// note that interceptions are taking place so we can adjust our
+// response URL expectations.
+if (!navigator.serviceWorker.controller.scriptURL.endsWith('empty.js')) {
+ window.isSWPresent = true;
+}
+testScript(location.search.substring(1) + ".js");
+</script>
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() function in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic.js");
+</script>
+</body>
+</html>
+
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",
+ "<res>hello</res>\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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() http fetching in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic_http.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() http fetching in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic_http.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() http fetching in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic_http.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() function in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() function in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1374943 - Test fetch cached redirects</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_cached_redirect.js");
+</script>
+</body>
+</html>
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() CORS mode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_cors.js");
+</script>
+</body>
+</html>
+
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,
+ "<res>hello pass</res>\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,
+ "<res>hello pass</res>\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,
+ "<res>hello pass</res>\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
+ ? ""
+ : "<res>hello pass</res>\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,
+ "<res>hello pass</res>\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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() CORS mode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_cors.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test fetch() CORS mode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_cors.js");
+</script>
+</body>
+</html>
+
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test fetch() rejects when CSP blocks</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script type="application/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function withFrame(url) {
+ return new Promise(resolve => {
+ let frame = document.createElement('iframe');
+ frame.addEventListener('load', _ => {
+ resolve(frame);
+ }, { once: true });
+ frame.src = url;
+ document.body.appendChild(frame);
+ });
+}
+
+function asyncTest(frame) {
+ return new Promise((resolve, reject) => {
+ addEventListener('message', evt => {
+ if (evt.data === 'REJECTED') {
+ resolve();
+ } else {
+ reject();
+ }
+ }, { once: true });
+ frame.contentWindow.postMessage('GO', '*');
+ });
+}
+
+withFrame('file_fetch_csp_block_frame.html').then(frame => {
+ asyncTest(frame).then(_ => {
+ ok(true, 'fetch rejected correctly');
+ }).catch(e => {
+ ok(false, 'fetch resolved when it should have been CSP blocked');
+ }).then(_ => {
+ frame.remove();
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</body>
+</html>
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test FetchObserver</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SpecialPowers.pushPrefEnv({"set": [["dom.fetchObserver.enabled", true ]]}, () => {
+ let ifr = document.createElement('iframe');
+ ifr.src = "file_fetch_observer.html";
+ document.body.appendChild(ifr);
+
+ onmessage = function(e) {
+ if (e.data.type == "finish") {
+ SimpleTest.finish();
+ return;
+ }
+
+ if (e.data.type == "check") {
+ ok(e.data.status, e.data.message);
+ return;
+ }
+
+ ok(false, "Something when wrong.");
+ }
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</body>
+</html>
+
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test fetch user control referrer policy Bug 1304623</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+ const SJS = "://example.com/tests/dom/base/test/referrer_testserver.sjs?";
+ const PARAMS = ["SCHEME_FROM", "SCHEME_TO", "CROSS_ORIGIN"];
+
+ const testCases = [
+ {ACTION: ["generate-fetch-user-control-policy-test"],
+ PREFS: [['network.http.referer.defaultPolicy', 0]],
+ TESTS: [
+ // 0. No referrer.
+ {NAME: 'default-policy-value-no-referrer-https-http',
+ DESC: 'default-policy-value-no-referrer-https-http',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'http',
+ RESULT: 'none'},
+ {NAME: 'default-policy-value-no-referrer-https-https',
+ DESC: 'default-policy-value-no-referrer-https-https',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'https',
+ RESULT: 'none'}],
+ },
+ {ACTION: ["generate-fetch-user-control-policy-test"],
+ PREFS: [['network.http.referer.defaultPolicy', 1]],
+ TESTS: [
+ // 1. Same origin.
+ {NAME: 'default-policy-value-same-origin-https-http',
+ DESC: 'default-policy-value-same-origin-https-http',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'http',
+ RESULT: 'none'},
+ {NAME: 'default-policy-value-same-origin-http-https',
+ DESC: 'default-policy-value-same-origin-http-https',
+ SCHEME_FROM: 'http',
+ SCHEME_TO: 'https',
+ RESULT: 'none'},
+ {NAME: 'default-policy-value-same-origin-https-https',
+ DESC: 'default-policy-value-same-origin-https-https',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'https',
+ RESULT: 'full'}],
+ },
+ {ACTION: ["generate-fetch-user-control-policy-test"],
+ PREFS: [['network.http.referer.defaultPolicy', 2]],
+ TESTS: [
+ // 2. strict-origin-when-cross-origin.
+ {NAME: 'default-policy-value-strict-origin-when-cross-origin-https-http',
+ DESC: 'default-policy-value-strict-origin-when-cross-origin-https-http',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'http',
+ RESULT: 'none'},
+ {NAME: 'default-policy-value-strict-origin-when-cross-origin-http-https',
+ DESC: 'default-policy-value-strict-origin-when-cross-origin-http-https',
+ SCHEME_FROM: 'http',
+ SCHEME_TO: 'https',
+ RESULT: 'origin'},
+ {NAME: 'default-policy-value-strict-origin-when-cross-origin-https-https-same-origin',
+ DESC: 'default-policy-value-strict-origin-when-cross-origin-https-https-same-origin',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'https',
+ RESULT: 'full'},
+ {NAME: 'default-policy-value-strict-origin-when-cross-origin-https-https-cross-origin',
+ DESC: 'default-policy-value-strict-origin-when-cross-origin-https-https-cross-origin',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'https',
+ CROSS_ORIGIN: 'true',
+ RESULT: 'origin'}],
+ },
+ {ACTION: ["generate-fetch-user-control-policy-test"],
+ PREFS: [['network.http.referer.defaultPolicy', 3]],
+ TESTS: [
+ // 3. Default no-referrer-when-downgrade.
+ {NAME: 'default-policy-value-no-referrer-when-downgrade-https-http',
+ DESC: 'default-policy-value-no-referrer-when-downgrade-https-http',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'http',
+ RESULT: 'none'},
+ {NAME: 'default-policy-value-no-referrer-when-downgrade-http-https',
+ DESC: 'default-policy-value-no-referrer-when-downgrade-http-https',
+ SCHEME_FROM: 'http',
+ SCHEME_TO: 'https',
+ RESULT: 'full'},
+ {NAME: 'default-policy-value-no-referrer-when-downgrade-https-https',
+ DESC: 'default-policy-value-no-referrer-when-downgrade-https-https',
+ SCHEME_FROM: 'https',
+ SCHEME_TO: 'https',
+ RESULT: 'full'}],
+ },
+ ];
+
+ </script>
+ <script type="application/javascript" src="/tests/dom/base/test/referrer_helper.js"></script>
+
+</head>
+<body onload="tests.next();">
+ <iframe id="testframe"></iframe>
+</body>
+</html>
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1109751 - Test FormData parsing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_formdataparsing.js");
+</script>
+</body>
+</html>
+
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" +
+ "<body>foobar\r\n\r\n</body>\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,
+ "<body>foobar\r\n\r\n</body>",
+ "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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1109751 - Test FormData parsing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_formdataparsing.js");
+</script>
+</body>
+</html>
+
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Fetch Headers - Basic</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_headers_common.js");
+</script>
+</body>
+</html>
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Fetch Headers - Basic</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript" src="test_headers_common.js"> </script>
+<script type="text/javascript">
+// Main thread specific tests because they need SpecialPowers. Expects
+// test_headers_common.js to already be loaded.
+
+function TestRequestHeaders() {
+ is(typeof Headers, "function", "Headers global constructor exists.");
+ var headers = new Headers();
+ ok(headers, "Constructed empty Headers object");
+ SpecialPowers.wrap(headers).guard = "request";
+ TestCoreBehavior(headers, "foo");
+ var forbidden = [
+ "Accept-Charset",
+ "Accept-Encoding",
+ "Access-Control-Request-Headers",
+ "Access-Control-Request-Method",
+ "Connection",
+ "Content-Length",
+ "Cookie",
+ "Cookie2",
+ "Date",
+ "DNT",
+ "Expect",
+ "Host",
+ "Keep-Alive",
+ "Origin",
+ "Referer",
+ "TE",
+ "Trailer",
+ "Transfer-Encoding",
+ "Upgrade",
+ "Via",
+ "Proxy-Authorization",
+ "Proxy-blarg",
+ "Proxy-",
+ "Sec-foo",
+ "Sec-"
+ ];
+
+ for (var i = 0, n = forbidden.length; i < n; ++i) {
+ var name = forbidden[i];
+ headers.append(name, "hmm");
+ checkNotHas(headers, name, "Should not be able to append " + name + " to request headers");
+ headers.set(name, "hmm");
+ checkNotHas(headers, name, "Should not be able to set " + name + " on request headers");
+ }
+}
+
+function TestRequestNoCorsHeaders() {
+ is(typeof Headers, "function", "Headers global constructor exists.");
+ var headers = new Headers();
+ ok(headers, "Constructed empty Headers object");
+ SpecialPowers.wrap(headers).guard = "request-no-cors";
+
+ headers.append("foo", "bar");
+ checkNotHas(headers, "foo", "Should not be able to append arbitrary headers to request-no-cors headers.");
+ headers.set("foo", "bar");
+ checkNotHas(headers, "foo", "Should not be able to set arbitrary headers on request-no-cors headers.");
+
+ var simpleNames = [
+ "Accept",
+ "Accept-Language",
+ "Content-Language"
+ ];
+
+ var simpleContentTypes = [
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ "text/plain",
+ "application/x-www-form-urlencoded; charset=utf-8",
+ "multipart/form-data; charset=utf-8",
+ "text/plain; charset=utf-8"
+ ];
+
+ for (var i = 0, n = simpleNames.length; i < n; ++i) {
+ var name = simpleNames[i];
+ headers.append(name, "hmm");
+ checkHas(headers, name, "Should be able to append " + name + " to request-no-cors headers");
+ headers.set(name, "hmm");
+ checkHas(headers, name, "Should be able to set " + name + " on request-no-cors headers");
+ }
+
+ for (var i = 0, n = simpleContentTypes.length; i < n; ++i) {
+ var value = simpleContentTypes[i];
+ headers.append("Content-Type", value);
+ checkHas(headers, "Content-Type", "Should be able to append " + value + " Content-Type to request-no-cors headers");
+ headers.delete("Content-Type");
+ headers.set("Content-Type", value);
+ checkHas(headers, "Content-Type", "Should be able to set " + value + " Content-Type on request-no-cors headers");
+ }
+}
+
+function TestResponseHeaders() {
+ is(typeof Headers, "function", "Headers global constructor exists.");
+ var headers = new Headers();
+ ok(headers, "Constructed empty Headers object");
+ SpecialPowers.wrap(headers).guard = "response";
+ TestCoreBehavior(headers, "foo");
+ var forbidden = [
+ "Set-Cookie",
+ "Set-Cookie2"
+ ];
+
+ for (var i = 0, n = forbidden.length; i < n; ++i) {
+ var name = forbidden[i];
+ headers.append(name, "hmm");
+ checkNotHas(headers, name, "Should not be able to append " + name + " to response headers");
+ headers.set(name, "hmm");
+ checkNotHas(headers, name, "Should not be able to set " + name + " on response headers");
+ }
+}
+
+function TestImmutableHeaders() {
+ is(typeof Headers, "function", "Headers global constructor exists.");
+ var headers = new Headers();
+ ok(headers, "Constructed empty Headers object");
+ TestCoreBehavior(headers, "foo");
+ headers.append("foo", "atleastone");
+
+ SpecialPowers.wrap(headers).guard = "immutable";
+
+ shouldThrow(function() {
+ headers.append("foo", "wat");
+ }, TypeError, "Should not be able to append to immutable headers");
+
+ shouldThrow(function() {
+ headers.set("foo", "wat");
+ }, TypeError, "Should not be able to set immutable headers");
+
+ shouldThrow(function() {
+ headers.delete("foo");
+ }, TypeError, "Should not be able to delete immutable headers");
+
+ checkHas(headers, "foo", "Should be able to check immutable headers");
+ ok(headers.get("foo"), "Should be able to get immutable headers");
+}
+
+TestRequestHeaders();
+TestRequestNoCorsHeaders();
+TestResponseHeaders();
+TestImmutableHeaders();
+</script>
+</body>
+</html>
+
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 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Fetch Headers - Basic</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_headers_common.js");
+</script>
+</body>
+</html>
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for ReadableStreams and Fetch</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common_readableStreams.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+
+async function tests() {
+ await SpecialPowers.pushPrefEnv({
+ "set": [["dom.caches.enabled", true],
+ ["dom.caches.testing.enabled", true],
+ ["dom.quotaManager.testing", true]]
+ });
+
+ await test_nativeStream(SAME_COMPARTMENT);
+ await test_nativeStream(IFRAME_COMPARTMENT);
+ await workify('test_nativeStream');
+
+ await test_timeout(SAME_COMPARTMENT);
+ await test_timeout(IFRAME_COMPARTMENT);
+ await workify('test_timeout');
+
+ await test_nonNativeStream(SAME_COMPARTMENT);
+ await test_nonNativeStream(IFRAME_COMPARTMENT);
+ await workify('test_nonNativeStream');
+
+ await test_pendingStream(SAME_COMPARTMENT);
+ await test_pendingStream(IFRAME_COMPARTMENT);
+ await workify('test_pendingStream');
+
+ await test_noUint8Array(SAME_COMPARTMENT);
+ await test_noUint8Array(IFRAME_COMPARTMENT);
+ await workify('test_noUint8Array');
+
+ // Bug 1746646: Make mochitests work with TCP enabled (cookieBehavior = 5)
+ // Acquire storage access permission here so that the cache works in xorigin
+ // tests. Otherwise, the iframe containing this page is isolated from
+ // first-party storage access, which isolates the caches object.
+ if (isXOrigin) {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await SpecialPowers.addPermission(
+ "storageAccessAPI",
+ true,
+ window.location.href
+ );
+ await SpecialPowers.wrap(document).requestStorageAccess();
+ }
+
+ await test_nativeStream_cache(SAME_COMPARTMENT);
+ await test_nativeStream_cache(IFRAME_COMPARTMENT);
+ await workify('test_nativeStream_cache');
+
+ await test_nonNativeStream_cache(SAME_COMPARTMENT);
+ await test_nonNativeStream_cache(IFRAME_COMPARTMENT);
+ await workify('test_nonNativeStream_cache');
+
+ await test_codeExecution(SAME_COMPARTMENT);
+ await test_codeExecution(IFRAME_COMPARTMENT);
+
+ await test_global(SAME_COMPARTMENT);
+ await test_global(IFRAME_COMPARTMENT);
+ await workify('test_global');
+}
+
+async function runTests() {
+ try {
+ await tests();
+ } catch (exc) {
+ ok(false, exc.toString());
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+// The iframe starts the tests by calling parent.next() when it loads.
+ </script>
+ <iframe src="iframe_readableStreams.html" id="iframe"></iframe>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Request object in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_request.js");
+</script>
+</body>
+</html>
+
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", "<invalid token??"];
+ for (var i = 0; i < forbidden.length; ++i) {
+ try {
+ var r = new Request("", { method: forbidden[i] });
+ ok(false, "Method " + forbidden[i] + " should be forbidden");
+ } catch (e) {
+ ok(true, "Method " + forbidden[i] + " should be forbidden");
+ }
+ }
+
+ var allowedNoCors = ["get", "head", "post"];
+ for (var i = 0; i < allowedNoCors.length; ++i) {
+ try {
+ var r = new Request("", { method: allowedNoCors[i], mode: "no-cors" });
+ ok(
+ true,
+ "Method " + allowedNoCors[i] + " should be allowed in no-cors mode"
+ );
+ } catch (e) {
+ ok(
+ false,
+ "Method " + allowedNoCors[i] + " should be allowed in no-cors mode"
+ );
+ }
+ }
+
+ var forbiddenNoCors = ["aardvark", "delete", "options", "put"];
+ for (var i = 0; i < forbiddenNoCors.length; ++i) {
+ try {
+ var r = new Request("", { method: forbiddenNoCors[i], mode: "no-cors" });
+ ok(
+ false,
+ "Method " + forbiddenNoCors[i] + " should be forbidden in no-cors mode"
+ );
+ } catch (e) {
+ ok(
+ true,
+ "Method " + forbiddenNoCors[i] + " should be forbidden in no-cors mode"
+ );
+ }
+ }
+
+ // HEAD/GET requests cannot have a body.
+ try {
+ var r = new Request("", { method: "get", body: "hello" });
+ ok(false, "HEAD/GET request cannot have a body");
+ } catch (e) {
+ is(e.name, "TypeError", "HEAD/GET request cannot have a body");
+ }
+
+ try {
+ var r = new Request("", { method: "head", body: "hello" });
+ ok(false, "HEAD/GET request cannot have a body");
+ } catch (e) {
+ is(e.name, "TypeError", "HEAD/GET request cannot have a body");
+ }
+ // Non HEAD/GET should not throw.
+ var r = new Request("", { method: "patch", body: "hello" });
+}
+function testUrlFragment() {
+ var req = new Request("./request#withfragment");
+ is(
+ req.url,
+ new URL("./request#withfragment", self.location.href).href,
+ "request.url should be serialized without exclude fragment flag set"
+ );
+}
+function testUrlMalformed() {
+ try {
+ var req = new Request("http:// example.com");
+ ok(
+ false,
+ "Creating a Request with a malformed URL should throw a TypeError"
+ );
+ } catch (e) {
+ is(
+ e.name,
+ "TypeError",
+ "Creating a Request with a malformed URL should throw a TypeError"
+ );
+ }
+}
+
+function testUrlCredentials() {
+ try {
+ var req = new Request("http://user@example.com");
+ ok(false, "URLs with credentials should be rejected");
+ } catch (e) {
+ is(e.name, "TypeError", "URLs with credentials should be rejected");
+ }
+
+ try {
+ var req = new Request("http://user:password@example.com");
+ ok(false, "URLs with credentials should be rejected");
+ } catch (e) {
+ is(e.name, "TypeError", "URLs with credentials should be rejected");
+ }
+}
+
+function testBodyUsed() {
+ var req = new Request("./bodyused", { method: "post", body: "Sample body" });
+ is(req.bodyUsed, false, "bodyUsed is initially false.");
+ return req
+ .text()
+ .then(v => {
+ 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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Make sure that Request.context is not exposed by default</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+var req = new Request("");
+ok(!("context" in req), "Request.context should not be exposed by default");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Request object in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_request.js");
+</script>
+</body>
+</html>
+
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test Response object in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_response.js");
+</script>
+</body>
+</html>
+
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Response ready to be used by wasm</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+
+const isCachingEnabled = SpecialPowers.getBoolPref("javascript.options.wasm_caching");
+
+async function runTests() {
+ let response = await fetch("/tests/dom/promise/tests/test_webassembly_compile_sample.wasm");
+ ok(!!response, "Fetch a wasm module produces a Response object");
+ is(response.headers.get("content-type"), "application/wasm", "Correct content-type");
+ if (!isCachingEnabled) {
+ ok(!SpecialPowers.wrap(response).hasCacheInfoChannel, "nsICacheInfoChannel not available");
+ SimpleTest.finish();
+ return;
+ }
+
+ ok(SpecialPowers.wrap(response).hasCacheInfoChannel, "nsICacheInfoChannel available");
+
+ let clonedResponse = response.clone();
+ ok(!!clonedResponse, "Cloned response");
+ is(clonedResponse.headers.get("content-type"), "application/wasm", "Correct content-type");
+ ok(SpecialPowers.wrap(clonedResponse).hasCacheInfoChannel, "nsICacheInfoChannel available");
+
+ response = await fetch(location.href);
+ ok(!!response, "Fetch another resource");
+ ok(response.headers.get("content-type") != "application/wasm", "Correct content-type");
+ ok(!SpecialPowers.wrap(response).hasCacheInfoChannel, "nsICacheInfoChannel available");
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTests();
+
+ </script>
+</body>
+</html>
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1039846 - Test Response object in worker</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="utils.js"> </script>
+<script type="text/javascript" src="sw_reroute.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_response.js");
+</script>
+</body>
+</html>
+
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 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1312410</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common_temporaryFileBlob.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <script type="application/javascript">
+
+var tests = [
+ // from common_temporaryFileBlob.js:
+ test_fetch_basic,
+ test_fetch_worker,
+ test_xhr_basic,
+ test_xhr_worker,
+ test_response_basic,
+ test_response_worker,
+ test_request_basic,
+ test_request_worker,
+];
+
+function next() {
+ if (!tests.length) {
+ SimpleTest.finish();
+ return;
+ }
+
+ var test = tests.shift();
+ test();
+}
+
+SpecialPowers.pushPrefEnv({ "set" : [[ "dom.blob.memoryToTemporaryFile", 1 ]] },
+ next);
+SimpleTest.waitForExplicitFinish();
+
+ </script>
+</body>
+</html>
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 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test WebAssembly.compileStreaming() and instantiateStreaming()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+<script type="text/javascript" src="fetch_test_framework.js"> </script>
+<script class="testbody" type="text/javascript">
+testScript("test_fetch_basic.js");
+</script>
+</body>
+</html>
+
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);
+ }
+});