aboutsummaryrefslogtreecommitdiff
path: root/test/js/web/fetch/fetch.test.ts
diff options
context:
space:
mode:
authorGravatar Ashcon Partovi <ashcon@partovi.net> 2023-03-07 12:22:34 -0800
committerGravatar GitHub <noreply@github.com> 2023-03-07 12:22:34 -0800
commitf7e4eb83694aa007a492ef66c28ffbe6a2dae791 (patch)
tree7af25aa5c42a2e1b2b47ba1df35f8caa9054cbeb /test/js/web/fetch/fetch.test.ts
parent36275a44ce7a33587bd26aad120042ab95470ff3 (diff)
downloadbun-f7e4eb83694aa007a492ef66c28ffbe6a2dae791.tar.gz
bun-f7e4eb83694aa007a492ef66c28ffbe6a2dae791.tar.zst
bun-f7e4eb83694aa007a492ef66c28ffbe6a2dae791.zip
Reorganize tests (#2332)
Diffstat (limited to 'test/js/web/fetch/fetch.test.ts')
-rw-r--r--test/js/web/fetch/fetch.test.ts935
1 files changed, 935 insertions, 0 deletions
diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts
new file mode 100644
index 000000000..1185dbd55
--- /dev/null
+++ b/test/js/web/fetch/fetch.test.ts
@@ -0,0 +1,935 @@
+import { serve, sleep } from "bun";
+import { afterAll, afterEach, beforeAll, describe, expect, it, beforeEach } from "bun:test";
+import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
+import { mkfifo } from "mkfifo";
+import { tmpdir } from "os";
+import { join } from "path";
+import { gc, withoutAggressiveGC } from "harness";
+
+const tmp_dir = mkdtempSync(join(realpathSync(tmpdir()), "fetch.test"));
+
+const fixture = readFileSync(join(import.meta.dir, "fetch.js.txt"), "utf8");
+
+let server;
+function startServer({ fetch, ...options }) {
+ server = serve({
+ ...options,
+ fetch,
+ port: 0,
+ });
+}
+
+afterEach(() => {
+ server?.stop?.(true);
+});
+
+afterAll(() => {
+ rmSync(tmp_dir, { force: true, recursive: true });
+});
+
+const payload = new Uint8Array(1024 * 1024 * 2);
+crypto.getRandomValues(payload);
+
+describe("AbortSignal", () => {
+ beforeEach(() => {
+ startServer({
+ async fetch(request) {
+ if (request.url.endsWith("/nodelay")) {
+ return new Response("Hello");
+ }
+ if (request.url.endsWith("/stream")) {
+ const reader = request.body.getReader();
+ const body = new ReadableStream({
+ async pull(controller) {
+ if (!reader) controller.close();
+ const { done, value } = await reader.read();
+ // When no more data needs to be consumed, close the stream
+ if (done) {
+ controller.close();
+ return;
+ }
+ // Enqueue the next data chunk into our target stream
+ controller.enqueue(value);
+ },
+ });
+ return new Response(body);
+ }
+ if (request.method.toUpperCase() === "POST") {
+ const body = await request.text();
+ return new Response(body);
+ }
+ await sleep(15);
+ return new Response("Hello");
+ },
+ });
+ });
+ afterEach(() => {
+ server?.stop?.(true);
+ });
+
+ it("AbortError", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ expect(async () => {
+ async function manualAbort() {
+ await sleep(1);
+ controller.abort();
+ }
+ await Promise.all([
+ fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
+ manualAbort(),
+ ]);
+ }).toThrow(new DOMException("The operation was aborted."));
+ });
+
+ it("AbortAfterFinish", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ await fetch(`http://127.0.0.1:${server.port}/nodelay`, { signal: signal }).then(async res =>
+ expect(await res.text()).toBe("Hello"),
+ );
+ controller.abort();
+ });
+
+ it("AbortErrorWithReason", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+
+ expect(async () => {
+ async function manualAbort() {
+ await sleep(10);
+ controller.abort(new Error("My Reason"));
+ }
+ await Promise.all([
+ fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
+ manualAbort(),
+ ]);
+ }).toThrow("My Reason");
+ });
+
+ it("AbortErrorEventListener", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ signal.addEventListener("abort", ev => {
+ const target = ev.currentTarget;
+ expect(target).toBeDefined();
+ expect(target.aborted).toBe(true);
+ expect(target.reason).toBeDefined();
+ expect(target.reason.name).toBe("AbortError");
+ });
+
+ expect(async () => {
+ async function manualAbort() {
+ await sleep(10);
+ controller.abort();
+ }
+ await Promise.all([
+ fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text()),
+ manualAbort(),
+ ]);
+ }).toThrow(new DOMException("The operation was aborted."));
+ });
+
+ it("AbortErrorWhileUploading", async () => {
+ const controller = new AbortController();
+
+ expect(async () => {
+ await fetch(`http://localhost:${server.port}`, {
+ method: "POST",
+ body: new ReadableStream({
+ pull(event_controller) {
+ event_controller.enqueue(new Uint8Array([1, 2, 3, 4]));
+ //this will abort immediately should abort before connected
+ controller.abort();
+ },
+ }),
+ signal: controller.signal,
+ });
+ }).toThrow(new DOMException("The operation was aborted."));
+ });
+
+ it("TimeoutError", async () => {
+ const signal = AbortSignal.timeout(10);
+
+ try {
+ await fetch(`http://127.0.0.1:${server.port}`, { signal: signal }).then(res => res.text());
+ expect(() => {}).toThrow();
+ } catch (ex: any) {
+ expect(ex.name).toBe("TimeoutError");
+ }
+ });
+
+ it("Request", async () => {
+ const controller = new AbortController();
+ const signal = controller.signal;
+ async function manualAbort() {
+ await sleep(10);
+ controller.abort();
+ }
+
+ try {
+ const request = new Request(`http://127.0.0.1:${server.port}`, { signal });
+ await Promise.all([fetch(request).then(res => res.text()), manualAbort()]);
+ expect(() => {}).toThrow();
+ } catch (ex: any) {
+ expect(ex.name).toBe("AbortError");
+ }
+ });
+});
+
+describe("Headers", () => {
+ it(".toJSON", () => {
+ const headers = new Headers({
+ "content-length": "123",
+ "content-type": "text/plain",
+ "x-another-custom-header": "Hello World",
+ "x-custom-header": "Hello World",
+ });
+ expect(JSON.stringify(headers.toJSON(), null, 2)).toBe(
+ JSON.stringify(Object.fromEntries(headers.entries()), null, 2),
+ );
+ });
+
+ it(".getSetCookie() with object", () => {
+ const headers = new Headers({
+ "content-length": "123",
+ "content-type": "text/plain",
+ "x-another-custom-header": "Hello World",
+ "x-custom-header": "Hello World",
+ "Set-Cookie": "foo=bar; Path=/; HttpOnly",
+ });
+ expect(headers.count).toBe(5);
+ expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly"]);
+ });
+
+ it(".getSetCookie() with array", () => {
+ const headers = new Headers([
+ ["content-length", "123"],
+ ["content-type", "text/plain"],
+ ["x-another-custom-header", "Hello World"],
+ ["x-custom-header", "Hello World"],
+ ["Set-Cookie", "foo=bar; Path=/; HttpOnly"],
+ ["Set-Cookie", "foo2=bar2; Path=/; HttpOnly"],
+ ]);
+ expect(headers.count).toBe(6);
+ expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly", "foo2=bar2; Path=/; HttpOnly"]);
+ });
+
+ it("Set-Cookies init", () => {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["Set-Cookie", "bar=baz"],
+ ["X-bun", "abc"],
+ ["X-bun", "def"],
+ ]);
+ const actual = [...headers];
+ expect(actual).toEqual([
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "bar=baz"],
+ ["x-bun", "abc, def"],
+ ]);
+ expect([...headers.values()]).toEqual(["foo=bar", "bar=baz", "abc, def"]);
+ });
+
+ it("Headers append multiple", () => {
+ const headers = new Headers([
+ ["Set-Cookie", "foo=bar"],
+ ["X-bun", "foo"],
+ ]);
+ headers.append("Set-Cookie", "bar=baz");
+ headers.append("x-bun", "bar");
+ const actual = [...headers];
+
+ // we do not preserve the order
+ // which is kind of bad
+ expect(actual).toEqual([
+ ["set-cookie", "foo=bar"],
+ ["set-cookie", "bar=baz"],
+ ["x-bun", "foo, bar"],
+ ]);
+ });
+
+ it("append duplicate set cookie key", () => {
+ const headers = new Headers([["Set-Cookie", "foo=bar"]]);
+ headers.append("set-Cookie", "foo=baz");
+ headers.append("Set-cookie", "baz=bar");
+ const actual = [...headers];
+ expect(actual).toEqual([
+ ["set-cookie", "foo=baz"],
+ ["set-cookie", "baz=bar"],
+ ]);
+ });
+
+ it("set duplicate cookie key", () => {
+ const headers = new Headers([["Set-Cookie", "foo=bar"]]);
+ headers.set("set-Cookie", "foo=baz");
+ headers.set("set-cookie", "bar=qat");
+ const actual = [...headers];
+ expect(actual).toEqual([
+ ["set-cookie", "foo=baz"],
+ ["set-cookie", "bar=qat"],
+ ]);
+ });
+});
+
+describe("fetch", () => {
+ const urls = [
+ "https://example.com",
+ "http://example.com",
+ new URL("https://example.com"),
+ new Request({ url: "https://example.com" }),
+ { toString: () => "https://example.com" },
+ ];
+ for (let url of urls) {
+ gc();
+ let name;
+ if (url instanceof URL) {
+ name = "URL: " + url;
+ } else if (url instanceof Request) {
+ name = "Request: " + url.url;
+ } else if (url.hasOwnProperty("toString")) {
+ name = "Object: " + url.toString();
+ } else {
+ name = url;
+ }
+ it(name, async () => {
+ gc();
+ const response = await fetch(url, { verbose: true });
+ gc();
+ const text = await response.text();
+ gc();
+ expect(fixture).toBe(text);
+ });
+ }
+
+ it('redirect: "manual"', async () => {
+ startServer({
+ fetch(req) {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ Location: "https://example.com",
+ },
+ });
+ },
+ });
+ const response = await fetch(`http://${server.hostname}:${server.port}`, {
+ redirect: "manual",
+ });
+ expect(response.status).toBe(302);
+ expect(response.headers.get("location")).toBe("https://example.com");
+ expect(response.redirected).toBe(true);
+ });
+
+ it('redirect: "follow"', async () => {
+ startServer({
+ fetch(req) {
+ return new Response(null, {
+ status: 302,
+ headers: {
+ Location: "https://example.com",
+ },
+ });
+ },
+ });
+ const response = await fetch(`http://${server.hostname}:${server.port}`, {
+ redirect: "follow",
+ });
+ expect(response.status).toBe(200);
+ expect(response.headers.get("location")).toBe(null);
+ expect(response.redirected).toBe(true);
+ });
+
+ it("provide body", async () => {
+ startServer({
+ fetch(req) {
+ return new Response(req.body);
+ },
+ host: "localhost",
+ });
+
+ // POST with body
+ const url = `http://${server.hostname}:${server.port}`;
+ const response = await fetch(url, { method: "POST", body: "buntastic" });
+ expect(response.status).toBe(200);
+ expect(await response.text()).toBe("buntastic");
+ });
+
+ ["GET", "HEAD", "OPTIONS"].forEach(method =>
+ it(`fail on ${method} with body`, async () => {
+ const url = `http://${server.hostname}:${server.port}`;
+ expect(async () => {
+ await fetch(url, { body: "buntastic" });
+ }).toThrow("fetch() request with GET/HEAD/OPTIONS method cannot have body.");
+ }),
+ );
+});
+
+it("simultaneous HTTPS fetch", async () => {
+ const urls = ["https://example.com", "https://www.example.com"];
+ for (let batch = 0; batch < 4; batch++) {
+ const promises = new Array(20);
+ for (let i = 0; i < 20; i++) {
+ promises[i] = fetch(urls[i % 2]);
+ }
+ const result = await Promise.all(promises);
+ expect(result.length).toBe(20);
+ for (let i = 0; i < 20; i++) {
+ expect(result[i].status).toBe(200);
+ expect(await result[i].text()).toBe(fixture);
+ }
+ }
+});
+
+it("website with tlsextname", async () => {
+ // irony
+ await fetch("https://bun.sh", { method: "HEAD" });
+});
+
+function testBlobInterface(blobbyConstructor, hasBlobFn?) {
+ for (let withGC of [false, true]) {
+ for (let jsonObject of [
+ { hello: true },
+ {
+ hello: "πŸ˜€ πŸ˜ƒ πŸ˜„ 😁 πŸ˜† πŸ˜… πŸ˜‚ 🀣 πŸ₯² ☺️ 😊 πŸ˜‡ πŸ™‚ πŸ™ƒ πŸ˜‰ 😌 😍 πŸ₯° 😘 πŸ˜— πŸ˜™ 😚 πŸ˜‹ πŸ˜› 😝 😜 πŸ€ͺ 🀨 🧐 πŸ€“ 😎 πŸ₯Έ 🀩 πŸ₯³",
+ },
+ ]) {
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} json${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> json${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+ expect(JSON.stringify(await response.json())).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> invalid json${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(
+ new TextEncoder().encode(JSON.stringify(jsonObject) + " NOW WE ARE INVALID JSON"),
+ );
+ if (withGC) gc();
+ var failed = false;
+ try {
+ await response.json();
+ } catch (e) {
+ failed = true;
+ }
+ expect(failed).toBe(true);
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} text${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ expect(await response.text()).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> text${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+ expect(await response.text()).toBe(JSON.stringify(jsonObject));
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+
+ var response = blobbyConstructor(JSON.stringify(jsonObject));
+ if (withGC) gc();
+
+ const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
+ if (withGC) gc();
+
+ const compare = new Uint8Array(await response.arrayBuffer());
+ if (withGC) gc();
+
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < compare.length; i++) {
+ if (withGC) gc();
+
+ expect(compare[i]).toBe(bytes[i]);
+ if (withGC) gc();
+ }
+ });
+ if (withGC) gc();
+ });
+
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} arrayBuffer -> arrayBuffer${
+ withGC ? " (with gc) " : ""
+ }`, async () => {
+ if (withGC) gc();
+
+ var response = blobbyConstructor(new TextEncoder().encode(JSON.stringify(jsonObject)));
+ if (withGC) gc();
+
+ const bytes = new TextEncoder().encode(JSON.stringify(jsonObject));
+ if (withGC) gc();
+
+ const compare = new Uint8Array(await response.arrayBuffer());
+ if (withGC) gc();
+
+ withoutAggressiveGC(() => {
+ for (let i = 0; i < compare.length; i++) {
+ if (withGC) gc();
+
+ expect(compare[i]).toBe(bytes[i]);
+ if (withGC) gc();
+ }
+ });
+ if (withGC) gc();
+ });
+
+ hasBlobFn &&
+ it(`${jsonObject.hello === true ? "latin1" : "utf16"} blob${withGC ? " (with gc) " : ""}`, async () => {
+ if (withGC) gc();
+ const text = JSON.stringify(jsonObject);
+ var response = blobbyConstructor(text);
+ if (withGC) gc();
+ const size = new TextEncoder().encode(text).byteLength;
+ if (withGC) gc();
+ const blobed = await response.blob();
+ if (withGC) gc();
+ expect(blobed instanceof Blob).toBe(true);
+ if (withGC) gc();
+ expect(blobed.size).toBe(size);
+ if (withGC) gc();
+ blobed.type = "";
+ if (withGC) gc();
+ expect(blobed.type).toBe("");
+ if (withGC) gc();
+ blobed.type = "application/json";
+ if (withGC) gc();
+ expect(blobed.type).toBe("application/json");
+ if (withGC) gc();
+ const out = await blobed.text();
+ expect(out).toBe(text);
+ if (withGC) gc();
+ await new Promise(resolve => setTimeout(resolve, 1));
+ if (withGC) gc();
+ expect(out).toBe(text);
+ const first = await blobed.arrayBuffer();
+ const initial = first[0];
+ first[0] = 254;
+ const second = await blobed.arrayBuffer();
+ expect(second[0]).toBe(initial);
+ expect(first[0]).toBe(254);
+ });
+ }
+ }
+}
+
+describe("Bun.file", () => {
+ let count = 0;
+ testBlobInterface(data => {
+ const blob = new Blob([data]);
+ const buffer = Bun.peek(blob.arrayBuffer());
+ const path = join(tmp_dir, `tmp-${count++}.bytes`);
+ writeFileSync(path, buffer);
+ const file = Bun.file(path);
+ expect(blob.size).toBe(file.size);
+ return file;
+ });
+
+ it("size is Infinity on a fifo", () => {
+ const path = join(tmp_dir, "test-fifo");
+ mkfifo(path);
+ const { size } = Bun.file(path);
+ expect(size).toBe(Infinity);
+ });
+
+ function forEachMethod(fn, skip?) {
+ const method = ["arrayBuffer", "text", "json"];
+ for (const m of method) {
+ (skip ? it.skip : it)(m, fn(m));
+ }
+ }
+
+ describe("bad permissions throws", () => {
+ const path = join(tmp_dir, "my-new-file");
+ beforeAll(async () => {
+ await Bun.write(path, "hey");
+ chmodSync(path, 0o000);
+ });
+
+ forEachMethod(
+ m => () => {
+ const file = Bun.file(path);
+ expect(async () => await file[m]()).toThrow("Permission denied");
+ },
+ () => {
+ try {
+ readFileSync(path);
+ } catch {
+ return false;
+ }
+ return true;
+ },
+ );
+ });
+
+ describe("non-existent file throws", () => {
+ const path = join(tmp_dir, "does-not-exist");
+
+ forEachMethod(m => async () => {
+ const file = Bun.file(path);
+ expect(async () => await file[m]()).toThrow("No such file or directory");
+ });
+ });
+});
+
+describe("Blob", () => {
+ testBlobInterface(data => new Blob([data]));
+
+ var blobConstructorValues = [
+ ["123", "456"],
+ ["123", 456],
+ ["123", "456", "789"],
+ ["123", 456, 789],
+ [1, 2, 3, 4, 5, 6, 7, 8, 9],
+ [Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 9])],
+ [Uint8Array.from([1, 2, 3, 4]), "5678", 9],
+ [new Blob([Uint8Array.from([1, 2, 3, 4])]), "5678", 9],
+ [
+ new Blob([
+ new TextEncoder().encode(
+ "πŸ˜€ πŸ˜ƒ πŸ˜„ 😁 πŸ˜† πŸ˜… πŸ˜‚ 🀣 πŸ₯² ☺️ 😊 πŸ˜‡ πŸ™‚ πŸ™ƒ πŸ˜‰ 😌 😍 πŸ₯° 😘 πŸ˜— πŸ˜™ 😚 πŸ˜‹ πŸ˜› 😝 😜 πŸ€ͺ 🀨 🧐 πŸ€“ 😎 πŸ₯Έ 🀩 πŸ₯³",
+ ),
+ ]),
+ ],
+ [
+ new TextEncoder().encode(
+ "πŸ˜€ πŸ˜ƒ πŸ˜„ 😁 πŸ˜† πŸ˜… πŸ˜‚ 🀣 πŸ₯² ☺️ 😊 πŸ˜‡ πŸ™‚ πŸ™ƒ πŸ˜‰ 😌 😍 πŸ₯° 😘 πŸ˜— πŸ˜™ 😚 πŸ˜‹ πŸ˜› 😝 😜 πŸ€ͺ 🀨 🧐 πŸ€“ 😎 πŸ₯Έ 🀩 πŸ₯³",
+ ),
+ ],
+ ];
+
+ var expected = [
+ "123456",
+ "123456",
+ "123456789",
+ "123456789",
+ "123456789",
+ "\x01\x02\x03\x04\x05\x06\x07\t",
+ "\x01\x02\x03\x0456789",
+ "\x01\x02\x03\x0456789",
+ "πŸ˜€ πŸ˜ƒ πŸ˜„ 😁 πŸ˜† πŸ˜… πŸ˜‚ 🀣 πŸ₯² ☺️ 😊 πŸ˜‡ πŸ™‚ πŸ™ƒ πŸ˜‰ 😌 😍 πŸ₯° 😘 πŸ˜— πŸ˜™ 😚 πŸ˜‹ πŸ˜› 😝 😜 πŸ€ͺ 🀨 🧐 πŸ€“ 😎 πŸ₯Έ 🀩 πŸ₯³",
+ "πŸ˜€ πŸ˜ƒ πŸ˜„ 😁 πŸ˜† πŸ˜… πŸ˜‚ 🀣 πŸ₯² ☺️ 😊 πŸ˜‡ πŸ™‚ πŸ™ƒ πŸ˜‰ 😌 😍 πŸ₯° 😘 πŸ˜— πŸ˜™ 😚 πŸ˜‹ πŸ˜› 😝 😜 πŸ€ͺ 🀨 🧐 πŸ€“ 😎 πŸ₯Έ 🀩 πŸ₯³",
+ ];
+
+ it(`blobConstructorValues`, async () => {
+ for (let i = 0; i < blobConstructorValues.length; i++) {
+ var response = new Blob(blobConstructorValues[i]);
+ const res = await response.text();
+ if (res !== expected[i]) {
+ throw new Error(
+ `Failed: ${expected[i].split("").map(a => a.charCodeAt(0))}, received: ${res
+ .split("")
+ .map(a => a.charCodeAt(0))}`,
+ );
+ }
+
+ expect(res).toBe(expected[i]);
+ }
+ });
+
+ for (let withGC of [false, true]) {
+ it(`Blob.slice() ${withGC ? " with gc" : ""}`, async () => {
+ var parts = ["hello", " ", "world"];
+ if (withGC) gc();
+ var str = parts.join("");
+ if (withGC) gc();
+ var combined = new Blob(parts);
+ if (withGC) gc();
+ for (let part of parts) {
+ if (withGC) gc();
+ expect(await combined.slice(str.indexOf(part), str.indexOf(part) + part.length).text()).toBe(part);
+ if (withGC) gc();
+ }
+ if (withGC) gc();
+ for (let part of parts) {
+ if (withGC) gc();
+ expect(await combined.slice(str.indexOf(part), str.indexOf(part) + part.length).text()).toBe(part);
+ if (withGC) gc();
+ }
+ });
+ }
+});
+
+{
+ const sample = new TextEncoder().encode("Hello World!");
+ const typedArrays = [
+ Uint8Array,
+ Uint8ClampedArray,
+ Int8Array,
+ Uint16Array,
+ Int16Array,
+ Uint32Array,
+ Int32Array,
+ Float32Array,
+ Float64Array,
+ ];
+ const Constructors = [Blob, Response, Request];
+
+ for (let withGC of [false, true]) {
+ for (let TypedArray of typedArrays) {
+ for (let Constructor of Constructors) {
+ it(`${Constructor.name} arrayBuffer() with ${TypedArray.name}${withGC ? " with gc" : ""}`, async () => {
+ const data = new TypedArray(sample);
+ if (withGC) gc();
+ const input = Constructor === Blob ? [data] : Constructor === Request ? { body: data } : data;
+ if (withGC) gc();
+ const blob = new Constructor(input);
+ if (withGC) gc();
+ const out = await blob.arrayBuffer();
+ if (withGC) gc();
+ expect(out instanceof ArrayBuffer).toBe(true);
+ if (withGC) gc();
+ expect(out.byteLength).toBe(data.byteLength);
+ if (withGC) gc();
+ });
+ }
+ }
+ }
+}
+
+describe("Response", () => {
+ describe("Response.json", () => {
+ it("works", async () => {
+ const inputs = ["hellooo", [[123], 456, 789], { hello: "world" }, { ok: "πŸ˜‰ 😌 😍 πŸ₯° 😘 " }];
+ for (let input of inputs) {
+ const output = JSON.stringify(input);
+ expect(await Response.json(input).text()).toBe(output);
+ }
+ // JSON.stringify() returns undefined
+ expect(await Response.json().text()).toBe("");
+ // JSON.stringify("") returns '""'
+ expect(await Response.json("").text()).toBe('""');
+ });
+ it("sets the content-type header", () => {
+ let response = Response.json("hello");
+ expect(response.type).toBe("basic");
+ expect(response.headers.get("content-type")).toBe("application/json;charset=utf-8");
+ expect(response.status).toBe(200);
+ });
+ it("supports number status code", () => {
+ let response = Response.json("hello", 407);
+ expect(response.type).toBe("basic");
+ expect(response.headers.get("content-type")).toBe("application/json;charset=utf-8");
+ expect(response.status).toBe(407);
+ });
+
+ it("supports headers", () => {
+ var response = Response.json("hello", {
+ headers: {
+ "content-type": "potato",
+ "x-hello": "world",
+ },
+ status: 408,
+ });
+
+ expect(response.headers.get("x-hello")).toBe("world");
+ expect(response.status).toBe(408);
+ });
+ });
+ describe("Response.redirect", () => {
+ it("works", () => {
+ const inputs = [
+ "http://example.com",
+ "http://example.com/",
+ "http://example.com/hello",
+ "http://example.com/hello/",
+ "http://example.com/hello/world",
+ "http://example.com/hello/world/",
+ ];
+ for (let input of inputs) {
+ expect(Response.redirect(input).headers.get("Location")).toBe(input);
+ }
+ });
+
+ it("supports headers", () => {
+ var response = Response.redirect("https://example.com", {
+ headers: {
+ "content-type": "potato",
+ "x-hello": "world",
+ Location: "https://wrong.com",
+ },
+ status: 408,
+ });
+ expect(response.headers.get("x-hello")).toBe("world");
+ expect(response.headers.get("Location")).toBe("https://example.com");
+ expect(response.status).toBe(302);
+ expect(response.type).toBe("basic");
+ expect(response.ok).toBe(false);
+ });
+ });
+ describe("Response.error", () => {
+ it("works", () => {
+ expect(Response.error().type).toBe("error");
+ expect(Response.error().ok).toBe(false);
+ expect(Response.error().status).toBe(0);
+ });
+ });
+ it("clone", async () => {
+ gc();
+ var body = new Response("<div>hello</div>", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ });
+ gc();
+ var clone = body.clone();
+ gc();
+ body.headers.set("content-type", "text/plain");
+ gc();
+ expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ expect(body.headers.get("content-type")).toBe("text/plain");
+ gc();
+ expect(await clone.text()).toBe("<div>hello</div>");
+ gc();
+ });
+ it("invalid json", async () => {
+ gc();
+ var body = new Response("<div>hello</div>", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ });
+ try {
+ await body.json();
+ expect(false).toBe(true);
+ } catch (exception) {
+ expect(exception instanceof SyntaxError).toBe(true);
+ }
+ });
+
+ testBlobInterface(data => new Response(data), true);
+});
+
+describe("Request", () => {
+ it("clone", async () => {
+ gc();
+ var body = new Request("https://hello.com", {
+ headers: {
+ "content-type": "text/html; charset=utf-8",
+ },
+ body: "<div>hello</div>",
+ });
+ gc();
+ expect(body.signal).toBeDefined();
+ gc();
+ expect(body.headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ var clone = body.clone();
+ gc();
+ expect(clone.signal).toBeDefined();
+ gc();
+ body.headers.set("content-type", "text/plain");
+ gc();
+ expect(clone.headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ expect(body.headers.get("content-type")).toBe("text/plain");
+ gc();
+ expect(await clone.text()).toBe("<div>hello</div>");
+ });
+
+ it("signal", async () => {
+ gc();
+ const controller = new AbortController();
+ const req = new Request("https://hello.com", { signal: controller.signal });
+ expect(req.signal.aborted).toBe(false);
+ gc();
+ controller.abort();
+ gc();
+ expect(req.signal.aborted).toBe(true);
+ });
+
+ it("cloned signal", async () => {
+ gc();
+ const controller = new AbortController();
+ const req = new Request("https://hello.com", { signal: controller.signal });
+ expect(req.signal.aborted).toBe(false);
+ gc();
+ controller.abort();
+ gc();
+ expect(req.signal.aborted).toBe(true);
+ gc();
+ const cloned = req.clone();
+ expect(cloned.signal.aborted).toBe(true);
+ });
+
+ testBlobInterface(data => new Request("https://hello.com", { body: data }), true);
+});
+
+describe("Headers", () => {
+ it("writes", async () => {
+ var headers = new Headers({
+ "content-type": "text/html; charset=utf-8",
+ });
+ gc();
+ expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
+ gc();
+ headers.delete("content-type");
+ gc();
+ expect(headers.get("content-type")).toBe(null);
+ gc();
+ headers.append("content-type", "text/plain");
+ gc();
+ expect(headers.get("content-type")).toBe("text/plain");
+ gc();
+ headers.append("content-type", "text/plain");
+ gc();
+ expect(headers.get("content-type")).toBe("text/plain, text/plain");
+ gc();
+ headers.set("content-type", "text/html; charset=utf-8");
+ gc();
+ expect(headers.get("content-type")).toBe("text/html; charset=utf-8");
+
+ headers.delete("content-type");
+ gc();
+ expect(headers.get("content-type")).toBe(null);
+ gc();
+ });
+});
+
+it("body nullable", async () => {
+ gc();
+ {
+ const req = new Request("https://hello.com", { body: null });
+ expect(req.body).toBeNull();
+ }
+ gc();
+ {
+ const req = new Request("https://hello.com", { body: undefined });
+ expect(req.body).toBeNull();
+ }
+ gc();
+ {
+ const req = new Request("https://hello.com");
+ expect(req.body).toBeNull();
+ }
+ gc();
+ {
+ const req = new Request("https://hello.com", { body: "" });
+ expect(req.body).not.toBeNull();
+ }
+});