aboutsummaryrefslogtreecommitdiff
path: root/test/js/web
diff options
context:
space:
mode:
Diffstat (limited to 'test/js/web')
-rw-r--r--test/js/web/abort/abort.signal.ts24
-rw-r--r--test/js/web/abort/abort.test.ts21
-rw-r--r--test/js/web/confirm-fixture.js3
-rw-r--r--test/js/web/console/console-log.expected.txt27
-rw-r--r--test/js/web/console/console-log.js18
-rw-r--r--test/js/web/encoding/text-decoder.test.js29
-rw-r--r--test/js/web/encoding/text-encoder.test.js2
-rw-r--r--test/js/web/fetch/blob.test.ts29
-rw-r--r--test/js/web/fetch/body-stream.test.ts29
-rw-r--r--test/js/web/fetch/fetch.stream.test.ts143
-rw-r--r--test/js/web/fetch/fetch.test.ts425
-rw-r--r--test/js/web/fetch/response.test.ts32
-rw-r--r--test/js/web/html/URLSearchParams.test.ts2
-rw-r--r--test/js/web/request/request.test.ts37
-rw-r--r--test/js/web/timers/performance.test.js25
-rw-r--r--test/js/web/url/url.test.ts66
-rw-r--r--test/js/web/web-globals.test.js41
-rw-r--r--test/js/web/worker.test.ts2
18 files changed, 945 insertions, 10 deletions
diff --git a/test/js/web/abort/abort.signal.ts b/test/js/web/abort/abort.signal.ts
new file mode 100644
index 000000000..da402f637
--- /dev/null
+++ b/test/js/web/abort/abort.signal.ts
@@ -0,0 +1,24 @@
+import type { Server } from "bun";
+
+const server = Bun.serve({
+ port: 0,
+ async fetch() {
+ const signal = AbortSignal.timeout(1);
+ return await fetch("https://bun.sh", { signal });
+ },
+});
+
+function hostname(server: Server) {
+ if (server.hostname.startsWith(":")) return `[${server.hostname}]`;
+ return server.hostname;
+}
+
+let url = `http://${hostname(server)}:${server.port}/`;
+
+const responses: Response[] = [];
+for (let i = 0; i < 10; i++) {
+ responses.push(await fetch(url));
+}
+server.stop(true);
+// we fail if any of the requests succeeded
+process.exit(responses.every(res => res.status === 500) ? 0 : 1);
diff --git a/test/js/web/abort/abort.test.ts b/test/js/web/abort/abort.test.ts
index 4895e0d13..ef0b07a18 100644
--- a/test/js/web/abort/abort.test.ts
+++ b/test/js/web/abort/abort.test.ts
@@ -18,4 +18,25 @@ describe("AbortSignal", () => {
expect(stderr?.toString()).not.toContain("✗");
});
+
+ test("AbortSignal.timeout(n) should not freeze the process", async () => {
+ const fileName = join(import.meta.dir, "abort.signal.ts");
+
+ const server = Bun.spawn({
+ cmd: [bunExe(), fileName],
+ env: bunEnv,
+ cwd: tmpdir(),
+ });
+
+ const exitCode = await Promise.race([
+ server.exited,
+ (async () => {
+ await Bun.sleep(5000);
+ server.kill();
+ return 2;
+ })(),
+ ]);
+
+ expect(exitCode).toBe(0);
+ });
});
diff --git a/test/js/web/confirm-fixture.js b/test/js/web/confirm-fixture.js
new file mode 100644
index 000000000..908570bce
--- /dev/null
+++ b/test/js/web/confirm-fixture.js
@@ -0,0 +1,3 @@
+const result = confirm("What is your answer?");
+
+console.error(result ? "Yes" : "No");
diff --git a/test/js/web/console/console-log.expected.txt b/test/js/web/console/console-log.expected.txt
index 332322665..39db03722 100644
--- a/test/js/web/console/console-log.expected.txt
+++ b/test/js/web/console/console-log.expected.txt
@@ -35,7 +35,7 @@ Promise { <pending> }
[Function]
[Function]
[class Foo]
-[class]
+[class (anonymous)]
{}
[Function: foooo]
/FooRegex/
@@ -50,3 +50,28 @@ String 123 should be 2nd word, 456 == 456 and percent s %s == What okay
[
{}, {}, {}, {}
]
+{
+ level1: {
+ level2: {
+ level3: [Object ...]
+ }
+ }
+}
+{
+ "1": [Object ...]
+}
+{
+ "1": [Object ...]
+}
+{
+ "1": {
+ "2": [Object ...]
+ }
+}
+{
+ "1": {
+ "2": {
+ "3": 3
+ }
+ }
+}
diff --git a/test/js/web/console/console-log.js b/test/js/web/console/console-log.js
index 4db40aaac..95f419781 100644
--- a/test/js/web/console/console-log.js
+++ b/test/js/web/console/console-log.js
@@ -58,3 +58,21 @@ infinteLoop.bar = infinteLoop;
console.log(infinteLoop, "am");
console.log(new Array(4).fill({}));
+const nestedObject = {
+ level1: {
+ level2: {
+ level3: {
+ level4: {
+ level5: {
+ name: "Deeply nested object",
+ },
+ },
+ },
+ },
+ },
+};
+console.log(nestedObject);
+console.dir({ 1: { 2: { 3: 3 } } }, { depth: 0, colors: false }, "Some ignored arg");
+console.dir({ 1: { 2: { 3: 3 } } }, { depth: -1, colors: false }, "Some ignored arg");
+console.dir({ 1: { 2: { 3: 3 } } }, { depth: 1.2, colors: false }, "Some ignored arg");
+console.dir({ 1: { 2: { 3: 3 } } }, { depth: Infinity, colors: false }, "Some ignored arg");
diff --git a/test/js/web/encoding/text-decoder.test.js b/test/js/web/encoding/text-decoder.test.js
index 4991cf361..3685a5f6d 100644
--- a/test/js/web/encoding/text-decoder.test.js
+++ b/test/js/web/encoding/text-decoder.test.js
@@ -250,7 +250,7 @@ describe("TextDecoder", () => {
it("constructor should set values", () => {
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
expect(decoder.fatal).toBe(true);
- // expect(decoder.ignoreBOM).toBe(false); // currently the getter for ignoreBOM doesn't work and always returns undefined
+ expect(decoder.ignoreBOM).toBe(false);
});
it("should throw on invalid input", () => {
@@ -258,6 +258,33 @@ describe("TextDecoder", () => {
const decoder = new TextDecoder("utf-8", { fatal: 10, ignoreBOM: {} });
}).toThrow();
});
+
+ it("should support undifined", () => {
+ const decoder = new TextDecoder(undefined);
+ expect(decoder.encoding).toBe("utf-8");
+ });
+});
+
+describe("TextDecoder ignoreBOM", () => {
+ it.each([
+ {
+ encoding: "utf-8",
+ bytes: [0xef, 0xbb, 0xbf, 0x61, 0x62, 0x63],
+ },
+ {
+ encoding: "utf-16le",
+ bytes: [0xff, 0xfe, 0x61, 0x00, 0x62, 0x00, 0x63, 0x00],
+ },
+ ])("should ignoreBOM for: %o", ({ encoding, bytes }) => {
+ const BOM = "\uFEFF";
+ const array = new Uint8Array(bytes);
+
+ const decoder_ignore_bom = new TextDecoder(encoding, { ignoreBOM: true });
+ expect(decoder_ignore_bom.decode(array)).toStrictEqual(`${BOM}abc`);
+
+ const decoder_not_ignore_bom = new TextDecoder(encoding, { ignoreBOM: false });
+ expect(decoder_not_ignore_bom.decode(array)).toStrictEqual("abc");
+ });
});
it("truncated sequences", () => {
diff --git a/test/js/web/encoding/text-encoder.test.js b/test/js/web/encoding/text-encoder.test.js
index 1bf2057bc..78940a6eb 100644
--- a/test/js/web/encoding/text-encoder.test.js
+++ b/test/js/web/encoding/text-encoder.test.js
@@ -111,7 +111,7 @@ describe("TextEncoder", () => {
const fixture = new Uint8Array(await Bun.file(import.meta.dir + "/utf8-encoding-fixture.bin").arrayBuffer());
const length = 0x110000;
let textEncoder = new TextEncoder();
- let textDecoder = new TextDecoder();
+ let textDecoder = new TextDecoder("utf-8", { ignoreBOM: true });
let encodeOut = new Uint8Array(length * 4);
let encodeIntoOut = new Uint8Array(length * 4);
let encodeIntoBuffer = new Uint8Array(4);
diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts
index ba44f8c1b..ea8d86a7a 100644
--- a/test/js/web/fetch/blob.test.ts
+++ b/test/js/web/fetch/blob.test.ts
@@ -1,6 +1,6 @@
import { test, expect } from "bun:test";
-test("Blob.slice", () => {
+test("Blob.slice", async () => {
const blob = new Blob(["Bun", "Foo"]);
const b1 = blob.slice(0, 3, "Text/HTML");
expect(b1 instanceof Blob).toBeTruthy();
@@ -26,6 +26,33 @@ test("Blob.slice", () => {
expect(blob.slice(null, "-123").size).toBe(6);
expect(blob.slice(0, 10).size).toBe(blob.size);
expect(blob.slice("text/plain;charset=utf-8").type).toBe("text/plain;charset=utf-8");
+
+ // test Blob.slice().slice(), issue#6252
+ expect(await blob.slice(0, 4).slice(0, 3).text()).toBe("Bun");
+ expect(await blob.slice(0, 4).slice(1, 3).text()).toBe("un");
+ expect(await blob.slice(1, 4).slice(0, 3).text()).toBe("unF");
+ expect(await blob.slice(1, 4).slice(1, 3).text()).toBe("nF");
+ expect(await blob.slice(1, 4).slice(2, 3).text()).toBe("F");
+ expect(await blob.slice(1, 4).slice(3, 3).text()).toBe("");
+ expect(await blob.slice(1, 4).slice(4, 3).text()).toBe("");
+ // test negative start
+ expect(await blob.slice(1, 4).slice(-1, 3).text()).toBe("F");
+ expect(await blob.slice(1, 4).slice(-2, 3).text()).toBe("nF");
+ expect(await blob.slice(1, 4).slice(-3, 3).text()).toBe("unF");
+ expect(await blob.slice(1, 4).slice(-4, 3).text()).toBe("unF");
+ expect(await blob.slice(1, 4).slice(-5, 3).text()).toBe("unF");
+ expect(await blob.slice(-1, 4).slice(-1, 3).text()).toBe("");
+ expect(await blob.slice(-2, 4).slice(-1, 3).text()).toBe("");
+ expect(await blob.slice(-3, 4).slice(-1, 3).text()).toBe("F");
+ expect(await blob.slice(-4, 4).slice(-1, 3).text()).toBe("F");
+ expect(await blob.slice(-5, 4).slice(-1, 3).text()).toBe("F");
+ expect(await blob.slice(-5, 4).slice(-2, 3).text()).toBe("nF");
+ expect(await blob.slice(-5, 4).slice(-3, 3).text()).toBe("unF");
+ expect(await blob.slice(-5, 4).slice(-4, 3).text()).toBe("unF");
+ expect(await blob.slice(-4, 4).slice(-3, 3).text()).toBe("nF");
+ expect(await blob.slice(-5, 4).slice(-4, 3).text()).toBe("unF");
+ expect(await blob.slice(-3, 4).slice(-2, 3).text()).toBe("F");
+ expect(await blob.slice(-blob.size, 4).slice(-blob.size, 3).text()).toBe("Bun");
});
test("new Blob", () => {
diff --git a/test/js/web/fetch/body-stream.test.ts b/test/js/web/fetch/body-stream.test.ts
index 8e2baf92a..8f7675528 100644
--- a/test/js/web/fetch/body-stream.test.ts
+++ b/test/js/web/fetch/body-stream.test.ts
@@ -13,6 +13,35 @@ var port = 0;
];
const useRequestObjectValues = [true, false];
+ test("Should not crash when not returning a promise when stream is in progress", async () => {
+ var called = false;
+ await runInServer(
+ {
+ async fetch() {
+ var stream = new ReadableStream({
+ type: "direct",
+ pull(controller) {
+ controller.write("hey");
+ setTimeout(() => {
+ controller.end();
+ }, 100);
+ },
+ });
+
+ return new Response(stream);
+ },
+ },
+ async url => {
+ called = true;
+ expect(await fetch(url).then(res => res.text())).toContain(
+ "Welcome to Bun! To get started, return a Response object.",
+ );
+ },
+ );
+
+ expect(called).toBe(true);
+ });
+
for (let RequestPrototypeMixin of BodyMixin) {
for (let useRequestObject of useRequestObjectValues) {
describe(`Request.prototoype.${RequestPrototypeMixin.name}() ${
diff --git a/test/js/web/fetch/fetch.stream.test.ts b/test/js/web/fetch/fetch.stream.test.ts
index 98271ee79..cc518e397 100644
--- a/test/js/web/fetch/fetch.stream.test.ts
+++ b/test/js/web/fetch/fetch.stream.test.ts
@@ -28,6 +28,142 @@ const smallText = Buffer.from("Hello".repeat(16));
const empty = Buffer.alloc(0);
describe("fetch() with streaming", () => {
+ it(`should be able to fail properly when reading from readable stream`, async () => {
+ let server: Server | null = null;
+ try {
+ server = Bun.serve({
+ port: 0,
+ async fetch(req) {
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(1000);
+ controller.enqueue("Hello, World!");
+ controller.close();
+ },
+ }),
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ },
+ );
+ },
+ });
+
+ const server_url = `http://${server.hostname}:${server.port}`;
+ try {
+ const res = await fetch(server_url, { signal: AbortSignal.timeout(20) });
+ const reader = res.body?.getReader();
+ while (true) {
+ const { done } = await reader?.read();
+ if (done) break;
+ }
+ expect(true).toBe("unreachable");
+ } catch (err: any) {
+ if (err.name !== "TimeoutError") throw err;
+ expect(err.message).toBe("The operation timed out.");
+ }
+ } finally {
+ server?.stop();
+ }
+ });
+
+ it(`should be locked after start buffering`, async () => {
+ let server: Server | null = null;
+ try {
+ server = Bun.serve({
+ port: 0,
+ fetch(req) {
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.close();
+ },
+ }),
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ },
+ );
+ },
+ });
+
+ const server_url = `http://${server.hostname}:${server.port}`;
+ const res = await fetch(server_url);
+ try {
+ const promise = res.text(); // start buffering
+ res.body?.getReader(); // get a reader
+ const result = await promise; // should throw the right error
+ expect(result).toBe("unreachable");
+ } catch (err: any) {
+ if (err.name !== "TypeError") throw err;
+ expect(err.message).toBe("ReadableStream is locked");
+ }
+ } finally {
+ server?.stop();
+ }
+ });
+
+ it(`should be locked after start buffering when calling getReader`, async () => {
+ let server: Server | null = null;
+ try {
+ server = Bun.serve({
+ port: 0,
+ fetch(req) {
+ return new Response(
+ new ReadableStream({
+ async start(controller) {
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.enqueue("Hello, World!");
+ await Bun.sleep(10);
+ controller.close();
+ },
+ }),
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "text/plain",
+ },
+ },
+ );
+ },
+ });
+
+ const server_url = `http://${server.hostname}:${server.port}`;
+ const res = await fetch(server_url);
+ try {
+ const body = res.body as ReadableStream<Uint8Array>;
+ const promise = res.text(); // start buffering
+ body.getReader(); // get a reader
+ const result = await promise; // should throw the right error
+ expect(result).toBe("unreachable");
+ } catch (err: any) {
+ if (err.name !== "TypeError") throw err;
+ expect(err.message).toBe("ReadableStream is locked");
+ }
+ } finally {
+ server?.stop();
+ }
+ });
+
it("can deflate with and without headers #4478", async () => {
let server: Server | null = null;
try {
@@ -77,7 +213,12 @@ describe("fetch() with streaming", () => {
.listen(0);
const address = server.address() as AddressInfo;
- const url = `http://${address.address}:${address.port}`;
+ let url;
+ if (address.family == "IPv4") {
+ url = `http://${address.address}:${address.port}`;
+ } else {
+ url = `http://[${address.address}]:${address.port}`;
+ }
async function getRequestLen(url: string) {
const response = await fetch(url);
const hasBody = response.body;
diff --git a/test/js/web/fetch/fetch.test.ts b/test/js/web/fetch/fetch.test.ts
index aa44ee76a..3ed20345b 100644
--- a/test/js/web/fetch/fetch.test.ts
+++ b/test/js/web/fetch/fetch.test.ts
@@ -3,8 +3,10 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, beforeEach } from
import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "fs";
import { mkfifo } from "mkfifo";
import { tmpdir } from "os";
+import { gzipSync } from "zlib";
import { join } from "path";
import { gc, withoutAggressiveGC, gcTick } from "harness";
+import net from "net";
const tmp_dir = mkdtempSync(join(realpathSync(tmpdir()), "fetch.test"));
@@ -93,6 +95,30 @@ describe("fetch data urls", () => {
expect(blob.type).toBe("text/plain;charset=utf-8");
expect(blob.text()).resolves.toBe("helloworld!");
});
+ it("unstrict parsing of invalid URL characters", async () => {
+ var url = "data:application/json,{%7B%7D}";
+ var res = await fetch(url);
+ expect(res.status).toBe(200);
+ expect(res.statusText).toBe("OK");
+ expect(res.ok).toBe(true);
+
+ var blob = await res.blob();
+ expect(blob.size).toBe(4);
+ expect(blob.type).toBe("application/json;charset=utf-8");
+ expect(blob.text()).resolves.toBe("{{}}");
+ });
+ it("unstrict parsing of double percent characters", async () => {
+ var url = "data:application/json,{%%7B%7D%%}%%";
+ var res = await fetch(url);
+ expect(res.status).toBe(200);
+ expect(res.statusText).toBe("OK");
+ expect(res.ok).toBe(true);
+
+ var blob = await res.blob();
+ expect(blob.size).toBe(9);
+ expect(blob.type).toBe("application/json;charset=utf-8");
+ expect(blob.text()).resolves.toBe("{%{}%%}%%");
+ });
it("data url (invalid)", async () => {
var url = "data:Hello%2C%20World!";
expect(async () => {
@@ -321,6 +347,27 @@ describe("Headers", () => {
expect(headers.getAll("set-cookie")).toEqual(["foo=bar; Path=/; HttpOnly"]);
});
+ it("presence of content-encoding header(issue #5668)", async () => {
+ startServer({
+ fetch(req) {
+ const content = gzipSync(JSON.stringify({ message: "Hello world" }));
+ return new Response(content, {
+ status: 200,
+ headers: {
+ "content-encoding": "gzip",
+ "content-type": "application/json",
+ },
+ });
+ },
+ });
+ const result = await fetch(`http://${server.hostname}:${server.port}/`);
+ const value = result.headers.get("content-encoding");
+ const body = await result.json();
+ expect(value).toBe("gzip");
+ expect(body).toBeDefined();
+ expect(body.message).toBe("Hello world");
+ });
+
it(".getSetCookie() with array", () => {
const headers = new Headers([
["content-length", "123"],
@@ -1216,6 +1263,16 @@ describe("Request", () => {
expect(req.signal.aborted).toBe(true);
});
+ it("copies method (#6144)", () => {
+ const request = new Request("http://localhost:1337/test", {
+ method: "POST",
+ });
+ const new_req = new Request(request, {
+ body: JSON.stringify({ message: "Hello world" }),
+ });
+ expect(new_req.method).toBe("POST");
+ });
+
it("cloned signal", async () => {
gc();
const controller = new AbortController();
@@ -1391,3 +1448,371 @@ it("cloned response headers are independent after accessing", () => {
cloned.headers.set("content-type", "text/plain");
expect(response.headers.get("content-type")).toBe("text/html; charset=utf-8");
});
+
+it("should work with http 100 continue", async () => {
+ let server: net.Server | undefined;
+ try {
+ server = net.createServer(socket => {
+ socket.on("data", data => {
+ const lines = data.toString().split("\r\n");
+ for (const line of lines) {
+ if (line.length == 0) {
+ socket.write("HTTP/1.1 100 Continue\r\n\r\n");
+ socket.write("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!");
+ break;
+ }
+ }
+ });
+ });
+
+ const { promise: start, resolve } = Promise.withResolvers();
+ server.listen(8080, resolve);
+
+ await start;
+
+ const address = server.address() as net.AddressInfo;
+ const result = await fetch(`http://localhost:${address.port}`).then(r => r.text());
+ expect(result).toBe("Hello, World!");
+ } finally {
+ server?.close();
+ }
+});
+
+it("should work with http 100 continue on the same buffer", async () => {
+ let server: net.Server | undefined;
+ try {
+ server = net.createServer(socket => {
+ socket.on("data", data => {
+ const lines = data.toString().split("\r\n");
+ for (const line of lines) {
+ if (line.length == 0) {
+ socket.write(
+ "HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n\r\nHello, World!",
+ );
+ break;
+ }
+ }
+ });
+ });
+
+ const { promise: start, resolve } = Promise.withResolvers();
+ server.listen(8080, resolve);
+
+ await start;
+
+ const address = server.address() as net.AddressInfo;
+ const result = await fetch(`http://localhost:${address.port}`).then(r => r.text());
+ expect(result).toBe("Hello, World!");
+ } finally {
+ server?.close();
+ }
+});
+
+describe("should strip headers", () => {
+ it("status code 303", async () => {
+ const server = Bun.serve({
+ port: 0,
+ async fetch(request: Request) {
+ if (request.url.endsWith("/redirect")) {
+ return new Response("hello", {
+ headers: {
+ ...request.headers,
+ "Location": "/redirected",
+ },
+ status: 303,
+ });
+ }
+
+ return new Response("hello", {
+ headers: request.headers,
+ });
+ },
+ });
+
+ const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, {
+ method: "POST",
+ headers: {
+ "I-Am-Here": "yes",
+ "Content-Language": "This should be stripped",
+ },
+ });
+
+ expect(headers.get("I-Am-Here")).toBe("yes");
+ expect(headers.get("Content-Language")).toBeNull();
+ expect(url).toEndWith("/redirected");
+ expect(redirected).toBe(true);
+ server.stop(true);
+ });
+
+ it("cross-origin status code 302", async () => {
+ const server1 = Bun.serve({
+ port: 0,
+ async fetch(request: Request) {
+ if (request.url.endsWith("/redirect")) {
+ return new Response("hello", {
+ headers: {
+ ...request.headers,
+ "Location": `http://${server2.hostname}:${server2.port}/redirected`,
+ },
+ status: 302,
+ });
+ }
+
+ return new Response("hello", {
+ headers: request.headers,
+ });
+ },
+ });
+
+ const server2 = Bun.serve({
+ port: 0,
+ async fetch(request: Request, server) {
+ if (request.url.endsWith("/redirect")) {
+ return new Response("hello", {
+ headers: {
+ ...request.headers,
+ "Location": `http://${server.hostname}:${server.port}/redirected`,
+ },
+ status: 302,
+ });
+ }
+
+ return new Response("hello", {
+ headers: request.headers,
+ });
+ },
+ });
+
+ const { headers, url, redirected } = await fetch(`http://${server1.hostname}:${server1.port}/redirect`, {
+ method: "GET",
+ headers: {
+ "Authorization": "yes",
+ },
+ });
+
+ expect(headers.get("Authorization")).toBeNull();
+ expect(url).toEndWith("/redirected");
+ expect(redirected).toBe(true);
+ server1.stop(true);
+ server2.stop(true);
+ });
+});
+
+it("same-origin status code 302 should not strip headers", async () => {
+ const server = Bun.serve({
+ port: 0,
+ async fetch(request: Request, server) {
+ if (request.url.endsWith("/redirect")) {
+ return new Response("hello", {
+ headers: {
+ ...request.headers,
+ "Location": `http://${server.hostname}:${server.port}/redirected`,
+ },
+ status: 302,
+ });
+ }
+
+ return new Response("hello", {
+ headers: request.headers,
+ });
+ },
+ });
+
+ const { headers, url, redirected } = await fetch(`http://${server.hostname}:${server.port}/redirect`, {
+ method: "GET",
+ headers: {
+ "Authorization": "yes",
+ },
+ });
+
+ expect(headers.get("Authorization")).toEqual("yes");
+ expect(url).toEndWith("/redirected");
+ expect(redirected).toBe(true);
+ server.stop(true);
+});
+
+describe("should handle relative location in the redirect, issue#5635", () => {
+ var server: Server;
+ beforeAll(async () => {
+ server = Bun.serve({
+ port: 0,
+ async fetch(request: Request) {
+ return new Response("Not Found", {
+ status: 404,
+ });
+ },
+ });
+ });
+ afterAll(() => {
+ server.stop(true);
+ });
+
+ it.each([
+ ["/a/b", "/c", "/c"],
+ ["/a/b", "c", "/a/c"],
+ ["/a/b", "/c/d", "/c/d"],
+ ["/a/b", "c/d", "/a/c/d"],
+ ["/a/b", "../c", "/c"],
+ ["/a/b", "../c/d", "/c/d"],
+ ["/a/b", "../../../c", "/c"],
+ // slash
+ ["/a/b/", "/c", "/c"],
+ ["/a/b/", "c", "/a/b/c"],
+ ["/a/b/", "/c/d", "/c/d"],
+ ["/a/b/", "c/d", "/a/b/c/d"],
+ ["/a/b/", "../c", "/a/c"],
+ ["/a/b/", "../c/d", "/a/c/d"],
+ ["/a/b/", "../../../c", "/c"],
+ ])("('%s', '%s')", async (pathname, location, expected) => {
+ server.reload({
+ async fetch(request: Request) {
+ const url = new URL(request.url);
+ if (url.pathname == pathname) {
+ return new Response("redirecting", {
+ headers: {
+ "Location": location,
+ },
+ status: 302,
+ });
+ } else if (url.pathname == expected) {
+ return new Response("Fine.");
+ }
+ return new Response("Not Found", {
+ status: 404,
+ });
+ },
+ });
+
+ const resp = await fetch(`http://${server.hostname}:${server.port}${pathname}`);
+ expect(resp.redirected).toBe(true);
+ expect(new URL(resp.url).pathname).toStrictEqual(expected);
+ expect(resp.status).toBe(200);
+ expect(await resp.text()).toBe("Fine.");
+ });
+});
+
+it("should throw RedirectURLTooLong when location is too long", async () => {
+ const server = Bun.serve({
+ port: 0,
+ async fetch(request: Request) {
+ gc();
+ const url = new URL(request.url);
+ if (url.pathname == "/redirect") {
+ return new Response("redirecting", {
+ headers: {
+ "Location": "B".repeat(8193),
+ },
+ status: 302,
+ });
+ }
+ return new Response("Not Found", {
+ status: 404,
+ });
+ },
+ });
+
+ let err = undefined;
+ try {
+ gc();
+ const resp = await fetch(`http://${server.hostname}:${server.port}/redirect`);
+ } catch (error) {
+ gc();
+ err = error;
+ }
+ expect(err).not.toBeUndefined();
+ expect(err).toBeInstanceOf(Error);
+ expect(err.code).toStrictEqual("RedirectURLTooLong");
+ server.stop(true);
+});
+
+it("304 not modified with missing content-length does not cause a request timeout", async () => {
+ const server = await Bun.listen({
+ socket: {
+ open(socket) {
+ socket.write("HTTP/1.1 304 Not Modified\r\n\r\n");
+ socket.flush();
+ setTimeout(() => {
+ socket.end();
+ }, 9999).unref();
+ },
+ data() {},
+ close() {},
+ },
+ port: 0,
+ hostname: "localhost",
+ });
+
+ const response = await fetch(`http://${server.hostname}:${server.port}/`);
+ expect(response.status).toBe(304);
+ expect(await response.arrayBuffer()).toHaveLength(0);
+ server.stop(true);
+});
+
+it("304 not modified with missing content-length and connection close does not cause a request timeout", async () => {
+ const server = await Bun.listen({
+ socket: {
+ open(socket) {
+ socket.write("HTTP/1.1 304 Not Modified\r\nConnection: close\r\n\r\n");
+ socket.flush();
+ setTimeout(() => {
+ socket.end();
+ }, 9999).unref();
+ },
+ data() {},
+ close() {},
+ },
+ port: 0,
+ hostname: "localhost",
+ });
+
+ const response = await fetch(`http://${server.hostname}:${server.port}/`);
+ expect(response.status).toBe(304);
+ expect(await response.arrayBuffer()).toHaveLength(0);
+ server.stop(true);
+});
+
+it("304 not modified with content-length 0 and connection close does not cause a request timeout", async () => {
+ const server = await Bun.listen({
+ socket: {
+ open(socket) {
+ socket.write("HTTP/1.1 304 Not Modified\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
+ socket.flush();
+ setTimeout(() => {
+ socket.end();
+ }, 9999).unref();
+ },
+ data() {},
+ close() {},
+ },
+ port: 0,
+ hostname: "localhost",
+ });
+
+ const response = await fetch(`http://${server.hostname}:${server.port}/`);
+ expect(response.status).toBe(304);
+ expect(await response.arrayBuffer()).toHaveLength(0);
+ server.stop(true);
+});
+
+it("304 not modified with 0 content-length does not cause a request timeout", async () => {
+ const server = await Bun.listen({
+ socket: {
+ open(socket) {
+ socket.write("HTTP/1.1 304 Not Modified\r\nContent-Length: 0\r\n\r\n");
+ socket.flush();
+ setTimeout(() => {
+ socket.end();
+ }, 9999).unref();
+ },
+ data() {},
+ close() {},
+ },
+ port: 0,
+ hostname: "localhost",
+ });
+
+ const response = await fetch(`http://${server.hostname}:${server.port}/`);
+ expect(response.status).toBe(304);
+ expect(await response.arrayBuffer()).toHaveLength(0);
+ server.stop(true);
+});
diff --git a/test/js/web/fetch/response.test.ts b/test/js/web/fetch/response.test.ts
new file mode 100644
index 000000000..ddfcd70f7
--- /dev/null
+++ b/test/js/web/fetch/response.test.ts
@@ -0,0 +1,32 @@
+import { describe, test, expect } from "bun:test";
+
+test("zero args returns an otherwise empty 200 response", () => {
+ const response = new Response();
+ expect(response.status).toBe(200);
+ expect(response.statusText).toBe("");
+});
+
+test("1-arg form returns a 200 response", () => {
+ const response = new Response("body text");
+
+ expect(response.status).toBe(200);
+ expect(response.statusText).toBe("");
+});
+
+describe("2-arg form", () => {
+ test("can fill in status/statusText, and it works", () => {
+ const response = new Response("body text", {
+ status: 202,
+ statusText: "Accepted.",
+ });
+
+ expect(response.status).toBe(202);
+ expect(response.statusText).toBe("Accepted.");
+ });
+ test('empty object continues to return 200/""', () => {
+ const response = new Response("body text", {});
+
+ expect(response.status).toBe(200);
+ expect(response.statusText).toBe("");
+ });
+});
diff --git a/test/js/web/html/URLSearchParams.test.ts b/test/js/web/html/URLSearchParams.test.ts
index a948af821..6de1e6be2 100644
--- a/test/js/web/html/URLSearchParams.test.ts
+++ b/test/js/web/html/URLSearchParams.test.ts
@@ -86,6 +86,8 @@ describe("URLSearchParams", () => {
// @ts-expect-error
expect(params.toJSON()).toEqual(props);
+
+ expect(Array.from(params.keys())).toHaveLength(params.size);
});
describe("non-standard extensions", () => {
diff --git a/test/js/web/request/request.test.ts b/test/js/web/request/request.test.ts
new file mode 100644
index 000000000..ac6008eb4
--- /dev/null
+++ b/test/js/web/request/request.test.ts
@@ -0,0 +1,37 @@
+import { test, expect } from "bun:test";
+
+test("request can receive undefined signal", async () => {
+ const request = new Request("http://example.com/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/bun;charset=utf-8",
+ },
+ body: "bun",
+ signal: undefined,
+ });
+ expect(request.method).toBe("POST");
+ // @ts-ignore
+ const clone = new Request(request);
+ expect(clone.method).toBe("POST");
+ expect(clone.headers.get("content-type")).toBe("text/bun;charset=utf-8");
+ expect(await request.text()).toBe("bun");
+ expect(await clone.text()).toBe("bun");
+});
+
+test("request can receive null signal", async () => {
+ const request = new Request("http://example.com/", {
+ method: "POST",
+ headers: {
+ "Content-Type": "text/bun;charset=utf-8",
+ },
+ body: "bun",
+ signal: null,
+ });
+ expect(request.method).toBe("POST");
+ // @ts-ignore
+ const clone = new Request(request);
+ expect(clone.method).toBe("POST");
+ expect(clone.headers.get("content-type")).toBe("text/bun;charset=utf-8");
+ expect(await request.text()).toBe("bun");
+ expect(await clone.text()).toBe("bun");
+});
diff --git a/test/js/web/timers/performance.test.js b/test/js/web/timers/performance.test.js
index dd50c4dc6..0d6cd577b 100644
--- a/test/js/web/timers/performance.test.js
+++ b/test/js/web/timers/performance.test.js
@@ -20,3 +20,28 @@ it("performance.now() should be monotonic", () => {
it("performance.timeOrigin + performance.now() should be similar to Date.now()", () => {
expect(Math.abs(performance.timeOrigin + performance.now() - Date.now()) < 1000).toBe(true);
});
+
+// https://github.com/oven-sh/bun/issues/5604
+it("performance.now() DOMJIT", () => {
+ // This test is very finnicky.
+ // It has to return true || return false to reproduce. Throwing an error doesn't work.
+ function run(start, prev) {
+ while (true) {
+ const current = performance.now();
+
+ if (Number.isNaN(current) || current < prev) {
+ return false;
+ }
+
+ if (current - start > 200) {
+ return true;
+ }
+ prev = current;
+ }
+ }
+
+ const start = performance.now();
+ if (!run(start, start)) {
+ throw new Error("performance.now() is not monotonic");
+ }
+});
diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts
index 6ad691c1b..f19a132df 100644
--- a/test/js/web/url/url.test.ts
+++ b/test/js/web/url/url.test.ts
@@ -1,6 +1,19 @@
import { describe, it, expect } from "bun:test";
describe("url", () => {
+ it("URL throws", () => {
+ expect(() => new URL("")).toThrow('"" cannot be parsed as a URL');
+ expect(() => new URL(" ")).toThrow('" " cannot be parsed as a URL');
+ expect(() => new URL("boop", "http!/example.com")).toThrow(
+ '"boop" cannot be parsed as a URL against "http!/example.com"',
+ );
+
+ // redact
+ expect(() => new URL("boop", "https!!username:password@example.com")).toThrow(
+ '"boop" cannot be parsed as a URL against <redacted>',
+ );
+ });
+
it("should have correct origin and protocol", () => {
var url = new URL("https://example.com");
expect(url.protocol).toBe("https:");
@@ -39,8 +52,7 @@ describe("url", () => {
expect(url.protocol).toBe("mailto:");
expect(url.origin).toBe("null");
});
- it.skip("should work with blob urls", () => {
- // TODO
+ it("blob urls", () => {
var url = new URL("blob:https://example.com/1234-5678");
expect(url.protocol).toBe("blob:");
expect(url.origin).toBe("https://example.com");
@@ -59,6 +71,9 @@ describe("url", () => {
url = new URL("blob:ws://example.com");
expect(url.protocol).toBe("blob:");
expect(url.origin).toBe("ws://example.com");
+ url = new URL("blob:file:///folder/else/text.txt");
+ expect(url.protocol).toBe("blob:");
+ expect(url.origin).toBe("file://");
});
it("prints", () => {
expect(Bun.inspect(new URL("https://example.com"))).toBe(`URL {
@@ -165,4 +180,51 @@ describe("url", () => {
expect(result.username).toBe(values.username);
}
});
+
+ describe("URL.canParse", () => {
+ (
+ [
+ {
+ "url": undefined,
+ "base": undefined,
+ "expected": false,
+ },
+ {
+ "url": "a:b",
+ "base": undefined,
+ "expected": true,
+ },
+ {
+ "url": undefined,
+ "base": "a:b",
+ "expected": false,
+ },
+ {
+ "url": "a:/b",
+ "base": undefined,
+ "expected": true,
+ },
+ {
+ "url": undefined,
+ "base": "a:/b",
+ "expected": true,
+ },
+ {
+ "url": "https://test:test",
+ "base": undefined,
+ "expected": false,
+ },
+ {
+ "url": "a",
+ "base": "https://b/",
+ "expected": true,
+ },
+ ] as const
+ ).forEach(({ url, base, expected }) => {
+ it(`URL.canParse(${url}, ${base})`, () => {
+ // @ts-expect-error
+ expect(URL.canParse(url, base)).toBe(expected);
+ });
+ });
+ });
});
diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js
index 1a4d7b1d1..74b28c0b0 100644
--- a/test/js/web/web-globals.test.js
+++ b/test/js/web/web-globals.test.js
@@ -1,6 +1,6 @@
-import { unsafe } from "bun";
+import { spawn } from "bun";
import { expect, it, test } from "bun:test";
-import { withoutAggressiveGC } from "harness";
+import { bunEnv, bunExe, withoutAggressiveGC } from "harness";
test("exists", () => {
expect(typeof URL !== "undefined").toBe(true);
@@ -31,6 +31,7 @@ const globalSetters = [
for (const [Constructor, name, eventName, prop] of globalSetters) {
test(`self.${name}`, () => {
var called = false;
+ console.log("name", name);
const callback = ({ [prop]: data }) => {
expect(data).toBe("hello");
@@ -232,3 +233,39 @@ test("navigator", () => {
expect(navigator.platform).toBe("Linux x86_64");
}
});
+
+test("confirm (yes)", async () => {
+ const proc = spawn({
+ cmd: [bunExe(), require("path").join(import.meta.dir, "./confirm-fixture.js")],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ proc.stdin.write("Y");
+ await proc.stdin.flush();
+
+ proc.stdin.write("\n");
+ await proc.stdin.flush();
+
+ await proc.exited;
+
+ expect(await new Response(proc.stderr).text()).toBe("Yes\n");
+});
+
+test("confirm (no)", async () => {
+ const proc = spawn({
+ cmd: [bunExe(), require("path").join(import.meta.dir, "./confirm-fixture.js")],
+ stderr: "pipe",
+ stdin: "pipe",
+ stdout: "pipe",
+ env: bunEnv,
+ });
+
+ proc.stdin.write("poask\n");
+ await proc.stdin.flush();
+ await proc.exited;
+
+ expect(await new Response(proc.stderr).text()).toBe("No\n");
+});
diff --git a/test/js/web/worker.test.ts b/test/js/web/worker.test.ts
index e1ab80487..8d7cf72d2 100644
--- a/test/js/web/worker.test.ts
+++ b/test/js/web/worker.test.ts
@@ -126,7 +126,7 @@ test("worker with event listeners doesnt close event loop", done => {
});
});
-test("worker with event listeners doesnt close event loop 2", done => {
+test("worker with event listeners doesn't close event loop 2", done => {
const x = Bun.spawn({
cmd: [bunExe(), path.join(import.meta.dir, "many-messages-event-loop.mjs"), "worker-fixture-many-messages2.js"],
env: bunEnv,