aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2023-05-26 16:28:09 -0700
committerGravatar GitHub <noreply@github.com> 2023-05-26 16:28:09 -0700
commit3a0735e16470cde796c1420f4db982af61bdab9c (patch)
treefd16595c122f085c5f18465f6db130e7f219dfbf
parent42125b43513a76e3e18b2b76ecc4323b1d8e1b3a (diff)
downloadbun-3a0735e16470cde796c1420f4db982af61bdab9c.tar.gz
bun-3a0735e16470cde796c1420f4db982af61bdab9c.tar.zst
bun-3a0735e16470cde796c1420f4db982af61bdab9c.zip
Pretty formatter for `Headers` & `URLSearchParams` (#3081)
* Pretty formatter for `Headers` & `URLSearchParams` * cleanup * console.log on Headers, FormData, URLSearchParams will always quote the keys now --------- Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
-rw-r--r--src/bun.js/bindings/JSDOMWrapper.h1
-rw-r--r--src/bun.js/bindings/bindings.zig4
-rw-r--r--src/bun.js/bindings/exports.zig75
-rw-r--r--src/bun.js/bindings/webcore/JSFetchHeaders.cpp2
-rw-r--r--src/bun.js/bindings/webcore/JSURLSearchParams.cpp58
-rw-r--r--src/bun.js/bindings/webcore/JSURLSearchParams.h2
-rw-r--r--src/bun.js/webcore/body.zig4
-rw-r--r--src/bun.js/webcore/request.zig14
-rw-r--r--src/bun.js/webcore/response.zig16
-rw-r--r--test/js/bun/util/inspect.test.js5
-rw-r--r--test/js/web/fetch/headers.test.ts41
-rw-r--r--test/js/web/html/FormData.test.ts24
-rw-r--r--test/js/web/html/URLSearchParams.test.ts63
-rw-r--r--test/js/web/url/url.test.ts32
14 files changed, 279 insertions, 62 deletions
diff --git a/src/bun.js/bindings/JSDOMWrapper.h b/src/bun.js/bindings/JSDOMWrapper.h
index 726b2e8bf..f80585e72 100644
--- a/src/bun.js/bindings/JSDOMWrapper.h
+++ b/src/bun.js/bindings/JSDOMWrapper.h
@@ -58,6 +58,7 @@ static const uint8_t JSCommentNodeType = JSNodeType | NodeConstants::COMMENT_NOD
static const uint8_t JSCDATASectionNodeType = JSNodeType | NodeConstants::CDATA_SECTION_NODE;
static const uint8_t JSAttrNodeType = JSNodeType | NodeConstants::ATTRIBUTE_NODE;
static const uint8_t JSElementType = 0b11110000 | NodeConstants::ELEMENT_NODE;
+static const uint8_t JSAsJSONType = JSElementType;
static_assert(JSDOMWrapperType > JSC::LastJSCObjectType, "JSC::JSType offers the highest bit.");
static_assert(NodeConstants::LastNodeType <= JSNodeTypeMask, "NodeType should be represented in 4bit.");
diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig
index 42f0c5344..3bf2b9172 100644
--- a/src/bun.js/bindings/bindings.zig
+++ b/src/bun.js/bindings/bindings.zig
@@ -2980,6 +2980,10 @@ pub const JSValue = enum(JSValueReprInt) {
Event = 0b11101111,
DOMWrapper = 0b11101110,
Blob = 0b11111100,
+
+ /// This means that we don't have Zig bindings for the type yet, but it
+ /// implements .toJSON()
+ JSAsJSONType = 0b11110000 | 1,
_,
pub fn canGet(this: JSType) bool {
diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig
index bfea93306..37fc7dabc 100644
--- a/src/bun.js/bindings/exports.zig
+++ b/src/bun.js/bindings/exports.zig
@@ -1182,6 +1182,7 @@ pub const ZigConsoleClient = struct {
depth: u16 = 0,
max_depth: u16 = 8,
quote_strings: bool = false,
+ quote_keys: bool = false,
failed: bool = false,
estimated_line_length: usize = 0,
always_newline_scope: bool = false,
@@ -1266,6 +1267,7 @@ pub const ZigConsoleClient = struct {
Promise,
JSON,
+ toJSON,
NativeCode,
ArrayBuffer,
@@ -1485,6 +1487,8 @@ pub const ZigConsoleClient = struct {
.GetterSetter, .CustomGetterSetter => .Getter,
+ .JSAsJSONType => .toJSON,
+
else => .JSON,
},
.cell = js_type,
@@ -1774,14 +1778,14 @@ pub const ZigConsoleClient = struct {
if (!is_symbol) {
// TODO: make this one pass?
- if (!key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.slice()), key.slice())) {
+ if (!key.is16Bit() and (!this.quote_keys and JSLexer.isLatin1Identifier(@TypeOf(key.slice()), key.slice()))) {
this.addForNewLine(key.len + 1);
writer.print(
comptime Output.prettyFmt("<r>{}<d>:<r> ", enable_ansi_colors),
.{key},
);
- } else if (key.is16Bit() and JSLexer.isLatin1Identifier(@TypeOf(key.utf16SliceAligned()), key.utf16SliceAligned())) {
+ } else if (key.is16Bit() and (!this.quote_keys and JSLexer.isLatin1Identifier(@TypeOf(key.utf16SliceAligned()), key.utf16SliceAligned()))) {
this.addForNewLine(key.len + 1);
writer.print(
@@ -2156,20 +2160,41 @@ pub const ZigConsoleClient = struct {
} else if (value.as(JSC.WebCore.Blob)) |blob| {
blob.writeFormat(ZigConsoleClient.Formatter, this, writer_, enable_ansi_colors) catch {};
return;
+ } else if (value.as(JSC.FetchHeaders) != null) {
+ if (value.get(this.globalThis, "toJSON")) |toJSONFunction| {
+ this.addForNewLine("Headers ".len);
+ writer.writeAll(comptime Output.prettyFmt("<r>Headers ", enable_ansi_colors));
+ const prev_quote_keys = this.quote_keys;
+ this.quote_keys = true;
+ defer this.quote_keys = prev_quote_keys;
+
+ return this.printAs(
+ .Object,
+ Writer,
+ writer_,
+ toJSONFunction.callWithThis(this.globalThis, value, &.{}),
+ .Object,
+ enable_ansi_colors,
+ );
+ }
} else if (value.as(JSC.DOMFormData) != null) {
- const toJSONFunction = value.get(this.globalThis, "toJSON").?;
-
- this.addForNewLine("FormData (entries) ".len);
- writer.writeAll(comptime Output.prettyFmt("<r><blue>FormData<r> <d>(entries)<r> ", enable_ansi_colors));
-
- return this.printAs(
- .Object,
- Writer,
- writer_,
- toJSONFunction.callWithThis(this.globalThis, value, &.{}),
- .Object,
- enable_ansi_colors,
- );
+ if (value.get(this.globalThis, "toJSON")) |toJSONFunction| {
+ const prev_quote_keys = this.quote_keys;
+ this.quote_keys = true;
+ defer this.quote_keys = prev_quote_keys;
+
+ return this.printAs(
+ .Object,
+ Writer,
+ writer_,
+ toJSONFunction.callWithThis(this.globalThis, value, &.{}),
+ .Object,
+ enable_ansi_colors,
+ );
+ }
+
+ // this case should never happen
+ return this.printAs(.Undefined, Writer, writer_, .undefined, .Cell, enable_ansi_colors);
} else if (value.as(JSC.API.Bun.Timer.TimerObject)) |timer| {
this.addForNewLine("Timeout(# ) ".len + bun.fmt.fastDigitCount(@intCast(u64, @max(timer.id, 0))));
if (timer.kind == .setInterval) {
@@ -2317,6 +2342,20 @@ pub const ZigConsoleClient = struct {
this.writeIndent(Writer, writer_) catch {};
writer.writeAll("}");
},
+ .toJSON => {
+ if (value.get(this.globalThis, "toJSON")) |func| {
+ const result = func.callWithThis(this.globalThis, value, &.{});
+ if (result.toError() == null) {
+ const prev_quote_keys = this.quote_keys;
+ this.quote_keys = true;
+ defer this.quote_keys = prev_quote_keys;
+ this.printAs(.Object, Writer, writer_, result, value.jsType(), enable_ansi_colors);
+ return;
+ }
+ }
+
+ writer.writeAll("{}");
+ },
.JSON => {
var str = ZigString.init("");
value.jsonStringify(this.globalThis, this.indent, &str);
@@ -2844,7 +2883,13 @@ pub const ZigConsoleClient = struct {
.GlobalObject => this.printAs(.GlobalObject, Writer, writer, value, result.cell, enable_ansi_colors),
.Private => this.printAs(.Private, Writer, writer, value, result.cell, enable_ansi_colors),
.Promise => this.printAs(.Promise, Writer, writer, value, result.cell, enable_ansi_colors),
+
+ // Call JSON.stringify on the value
.JSON => this.printAs(.JSON, Writer, writer, value, result.cell, enable_ansi_colors),
+
+ // Call value.toJSON() and print as an object
+ .toJSON => this.printAs(.toJSON, Writer, writer, value, result.cell, enable_ansi_colors),
+
.NativeCode => this.printAs(.NativeCode, Writer, writer, value, result.cell, enable_ansi_colors),
.JSX => this.printAs(.JSX, Writer, writer, value, result.cell, enable_ansi_colors),
.Event => this.printAs(.Event, Writer, writer, value, result.cell, enable_ansi_colors),
diff --git a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp
index 2e75cdc86..6bea5dc84 100644
--- a/src/bun.js/bindings/webcore/JSFetchHeaders.cpp
+++ b/src/bun.js/bindings/webcore/JSFetchHeaders.cpp
@@ -301,7 +301,7 @@ static const HashTableValue JSFetchHeadersPrototypeTableValues[] = {
{ "values"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_values, 0 } },
{ "forEach"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_forEach, 1 } },
{ "toJSON"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_toJSON, 0 } },
- { "count"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsFetchHeadersGetterCount, 0 } },
+ { "count"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete), NoIntrinsic, { HashTableValue::GetterSetterType, jsFetchHeadersGetterCount, 0 } },
// { "getSetCookie"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsFetchHeadersPrototypeFunction_getSetCookie, 0 } },
};
diff --git a/src/bun.js/bindings/webcore/JSURLSearchParams.cpp b/src/bun.js/bindings/webcore/JSURLSearchParams.cpp
index d80304684..a99587d40 100644
--- a/src/bun.js/bindings/webcore/JSURLSearchParams.cpp
+++ b/src/bun.js/bindings/webcore/JSURLSearchParams.cpp
@@ -56,6 +56,7 @@
#include "wtf/URL.h"
#include "wtf/Vector.h"
#include <variant>
+#include "GCDefferalContext.h"
namespace WebCore {
using namespace JSC;
@@ -69,6 +70,7 @@ static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_getAll);
static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_has);
static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_set);
static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_sort);
+static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_toJSON);
static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_entries);
static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_keys);
static JSC_DECLARE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_values);
@@ -178,6 +180,7 @@ static const HashTableValue JSURLSearchParamsPrototypeTableValues[] = {
{ "values"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsURLSearchParamsPrototypeFunction_values, 0 } },
{ "forEach"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsURLSearchParamsPrototypeFunction_forEach, 1 } },
{ "toString"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsURLSearchParamsPrototypeFunction_toString, 0 } },
+ { "toJSON"_s, static_cast<unsigned>(JSC::PropertyAttribute::Function), NoIntrinsic, { HashTableValue::NativeFunctionType, jsURLSearchParamsPrototypeFunction_toJSON, 0 } },
{ "length"_s, static_cast<unsigned>(JSC::PropertyAttribute::CustomAccessor | JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontDelete | JSC::PropertyAttribute::DontEnum), NoIntrinsic, { HashTableValue::GetterSetterType, jsURLSearchParamsPrototype_getLength, 0 } },
};
@@ -393,6 +396,60 @@ JSC_DEFINE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_toString, (JSGlobalO
return IDLOperation<JSURLSearchParams>::call<jsURLSearchParamsPrototypeFunction_toStringBody>(*lexicalGlobalObject, *callFrame, "toString");
}
+static inline JSC::EncodedJSValue jsURLSearchParamsPrototypeFunction_toJSONBody(JSC::JSGlobalObject* lexicalGlobalObject, JSC::CallFrame* callFrame, typename IDLOperation<JSURLSearchParams>::ClassParameter castedThis)
+{
+ auto& vm = JSC::getVM(lexicalGlobalObject);
+ auto throwScope = DECLARE_THROW_SCOPE(vm);
+ UNUSED_PARAM(throwScope);
+ UNUSED_PARAM(callFrame);
+ auto& impl = castedThis->wrapped();
+ auto iter = impl.createIterator();
+
+ auto* obj = JSC::constructEmptyObject(lexicalGlobalObject, lexicalGlobalObject->objectPrototype(), impl.size() + 1);
+ obj->putDirect(vm, vm.propertyNames->toStringTagSymbol, jsNontrivialString(lexicalGlobalObject->vm(), "URLSearchParams"_s), JSC::PropertyAttribute::DontEnum | JSC::PropertyAttribute::ReadOnly | 0);
+
+ RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+ WTF::HashSet<String> seenKeys;
+ for (auto entry = iter.next(); entry.has_value(); entry = iter.next()) {
+ auto& key = entry.value().key;
+ auto& value = entry.value().value;
+ auto ident = Identifier::fromString(vm, key);
+ if (seenKeys.contains(key)) {
+ JSValue jsValue = obj->getDirect(vm, ident);
+ if (jsValue.isString()) {
+ GCDeferralContext deferralContext(lexicalGlobalObject->vm());
+ JSC::ObjectInitializationScope initializationScope(lexicalGlobalObject->vm());
+
+ JSC::JSArray* array = JSC::JSArray::tryCreateUninitializedRestricted(
+ initializationScope, &deferralContext,
+ lexicalGlobalObject->arrayStructureForIndexingTypeDuringAllocation(JSC::ArrayWithContiguous),
+ 2);
+
+ array->initializeIndex(initializationScope, 0, jsValue);
+ array->initializeIndex(initializationScope, 1, jsString(vm, value));
+ obj->putDirect(vm, ident, array, 0);
+ } else if (jsValue.isObject() && jsValue.getObject()->inherits<JSC::JSArray>()) {
+ JSC::JSArray* array = jsCast<JSC::JSArray*>(jsValue.getObject());
+ array->push(lexicalGlobalObject, jsString(vm, value));
+ RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
+
+ } else {
+ RELEASE_ASSERT_NOT_REACHED();
+ }
+ } else {
+ seenKeys.add(key);
+ obj->putDirect(vm, ident, jsString(vm, value), 0);
+ }
+ }
+
+ RELEASE_AND_RETURN(throwScope, JSValue::encode(obj));
+}
+
+JSC_DEFINE_HOST_FUNCTION(jsURLSearchParamsPrototypeFunction_toJSON, (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame))
+{
+ return IDLOperation<JSURLSearchParams>::call<jsURLSearchParamsPrototypeFunction_toJSONBody>(*lexicalGlobalObject, *callFrame, "toJSON");
+}
+
struct URLSearchParamsIteratorTraits {
static constexpr JSDOMIteratorType type = JSDOMIteratorType::Map;
using KeyType = IDLUSVString;
@@ -566,5 +623,4 @@ URLSearchParams* JSURLSearchParams::toWrapped(JSC::VM& vm, JSC::JSValue value)
return &wrapper->wrapped();
return nullptr;
}
-
}
diff --git a/src/bun.js/bindings/webcore/JSURLSearchParams.h b/src/bun.js/bindings/webcore/JSURLSearchParams.h
index b7e90a3fb..54abe1237 100644
--- a/src/bun.js/bindings/webcore/JSURLSearchParams.h
+++ b/src/bun.js/bindings/webcore/JSURLSearchParams.h
@@ -45,7 +45,7 @@ public:
static JSC::Structure* createStructure(JSC::VM& vm, JSC::JSGlobalObject* globalObject, JSC::JSValue prototype)
{
- return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::ObjectType, StructureFlags), info(), JSC::NonArray);
+ return JSC::Structure::create(vm, globalObject, prototype, JSC::TypeInfo(JSC::JSType(WebCore::JSAsJSONType), StructureFlags), info(), JSC::NonArray);
}
static JSC::JSValue getConstructor(JSC::VM&, const JSC::JSGlobalObject*);
diff --git a/src/bun.js/webcore/body.zig b/src/bun.js/webcore/body.zig
index b39fb1761..9cfa1f9ce 100644
--- a/src/bun.js/webcore/body.zig
+++ b/src/bun.js/webcore/body.zig
@@ -79,7 +79,7 @@ pub const Body = struct {
const Writer = @TypeOf(writer);
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("bodyUsed: ");
+ try writer.writeAll(comptime Output.prettyFmt("<r>bodyUsed<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.value == .Used), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
@@ -92,7 +92,7 @@ pub const Body = struct {
// }
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("status: ");
+ try writer.writeAll(comptime Output.prettyFmt("<r>status<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Double, Writer, writer, JSC.JSValue.jsNumber(this.init.status_code), .NumberObject, enable_ansi_colors);
if (this.value == .Blob) {
try formatter.printComma(Writer, writer, enable_ansi_colors);
diff --git a/src/bun.js/webcore/request.zig b/src/bun.js/webcore/request.zig
index 248b6a1bf..643fbb38b 100644
--- a/src/bun.js/webcore/request.zig
+++ b/src/bun.js/webcore/request.zig
@@ -133,18 +133,24 @@ pub const Request = struct {
defer formatter.indent -|= 1;
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("method: \"");
+ try writer.writeAll(comptime Output.prettyFmt("<r>method<d>:<r> \"", enable_ansi_colors));
+
try writer.writeAll(bun.asByteSlice(@tagName(this.method)));
try writer.writeAll("\"");
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("url: \"");
+ try writer.writeAll(comptime Output.prettyFmt("<r>url<d>:<r> ", enable_ansi_colors));
try this.ensureURL();
- try writer.print(comptime Output.prettyFmt("<r><b>{s}<r>", enable_ansi_colors), .{this.url});
+ try writer.print(comptime Output.prettyFmt("\"<b>{s}<r>\"", enable_ansi_colors), .{this.url});
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll(comptime Output.prettyFmt("<r>headers<d>:<r> ", enable_ansi_colors));
+ formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors);
- try writer.writeAll("\"");
if (this.body.value == .Blob) {
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
diff --git a/src/bun.js/webcore/response.zig b/src/bun.js/webcore/response.zig
index af69b8f95..8d1bfb961 100644
--- a/src/bun.js/webcore/response.zig
+++ b/src/bun.js/webcore/response.zig
@@ -120,7 +120,7 @@ pub const Response = struct {
pub const Props = struct {};
- pub fn writeFormat(this: *const Response, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
+ pub fn writeFormat(this: *Response, comptime Formatter: type, formatter: *Formatter, writer: anytype, comptime enable_ansi_colors: bool) !void {
const Writer = @TypeOf(writer);
try writer.print("Response ({}) {{\n", .{bun.fmt.size(this.body.len())});
{
@@ -128,26 +128,32 @@ pub const Response = struct {
defer formatter.indent -|= 1;
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("ok: ");
+ try writer.writeAll(comptime Output.prettyFmt("<r>ok<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.isOK()), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("url: \"");
+ try writer.writeAll(comptime Output.prettyFmt("<r>url<d>:<r> \"", enable_ansi_colors));
try writer.print(comptime Output.prettyFmt("<r><b>{s}<r>", enable_ansi_colors), .{this.url});
try writer.writeAll("\"");
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("statusText: ");
+ try writer.writeAll(comptime Output.prettyFmt("<r>headers<d>:<r> ", enable_ansi_colors));
+ formatter.printAs(.Private, Writer, writer, this.getHeaders(formatter.globalThis), .DOMWrapper, enable_ansi_colors);
+ formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
+ try writer.writeAll("\n");
+
+ try formatter.writeIndent(Writer, writer);
+ try writer.writeAll(comptime Output.prettyFmt("<r>statusText<d>:<r> ", enable_ansi_colors));
try JSPrinter.writeJSONString(this.status_text, Writer, writer, .ascii);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
try formatter.writeIndent(Writer, writer);
- try writer.writeAll("redirected: ");
+ try writer.writeAll(comptime Output.prettyFmt("<r>redirected<d>:<r> ", enable_ansi_colors));
formatter.printAs(.Boolean, Writer, writer, JSC.JSValue.jsBoolean(this.redirected), .BooleanObject, enable_ansi_colors);
formatter.printComma(Writer, writer, enable_ansi_colors) catch unreachable;
try writer.writeAll("\n");
diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js
index 5c67999fe..e91204c69 100644
--- a/test/js/bun/util/inspect.test.js
+++ b/test/js/bun/util/inspect.test.js
@@ -54,6 +54,7 @@ it("Blob inspect", () => {
expect(Bun.inspect(new Response(new Blob()))).toBe(`Response (0 KB) {
ok: true,
url: "",
+ headers: Headers {},
statusText: "",
redirected: false,
bodyUsed: false,
@@ -63,6 +64,7 @@ it("Blob inspect", () => {
expect(Bun.inspect(new Response("Hello"))).toBe(`Response (5 bytes) {
ok: true,
url: "",
+ headers: Headers {},
statusText: "",
redirected: false,
bodyUsed: false,
@@ -105,7 +107,8 @@ it("Request object", () => {
`
Request (0 KB) {
method: "GET",
- url: "https://example.com"
+ url: "https://example.com",
+ headers: Headers {}
}`.trim(),
);
});
diff --git a/test/js/web/fetch/headers.test.ts b/test/js/web/fetch/headers.test.ts
index 48cd3ad35..8ae17c11e 100644
--- a/test/js/web/fetch/headers.test.ts
+++ b/test/js/web/fetch/headers.test.ts
@@ -412,10 +412,46 @@ describe("Headers", () => {
]);
});
});
- describe("toJSON()", () => {
+ describe("Bun.inspect()", () => {
const it = "toJSON" in new Headers() ? test : test.skip;
it("can convert to json when empty", () => {
const headers = new Headers();
+ expect(Bun.inspect(headers)).toStrictEqual(`Headers {}`);
+ });
+ it("can convert to json", () => {
+ const headers = new Headers({
+ "cache-control": "public, immutable",
+ });
+ expect(Bun.inspect(headers)).toStrictEqual(
+ "Headers {" + "\n " + `"cache-control": "public, immutable"` + "\n" + "}",
+ );
+ });
+ it("can convert to json normalized", () => {
+ const headers = new Headers({
+ "user-agent": "bun",
+ "X-Custom-Header": "1",
+ "cache-control": "public, immutable",
+ });
+ expect(Bun.inspect(headers)).toStrictEqual(
+ "Headers " +
+ JSON.stringify(
+ {
+ "user-agent": "bun",
+ "cache-control": "public, immutable",
+ "x-custom-header": "1",
+ },
+ null,
+ 2,
+ ),
+ );
+ });
+ });
+ describe("toJSON()", () => {
+ // @ts-ignore
+ const it = new Headers()?.toJSON ? test : test.skip;
+
+ it("can convert to json when empty", () => {
+ const headers = new Headers();
expect(headers.toJSON()).toStrictEqual({});
});
it("can convert to json", () => {
@@ -440,7 +476,8 @@ describe("Headers", () => {
});
});
describe("count", () => {
- const it = "count" in new Headers() ? test : test.skip;
+ // @ts-ignore
+ const it = typeof new Headers()?.count !== "undefined" ? test : test.skip;
it("can count headers when empty", () => {
const headers = new Headers();
expect(headers.count).toBe(0);
diff --git a/test/js/web/html/FormData.test.ts b/test/js/web/html/FormData.test.ts
index af2871b10..abb298c1a 100644
--- a/test/js/web/html/FormData.test.ts
+++ b/test/js/web/html/FormData.test.ts
@@ -338,6 +338,30 @@ describe("FormData", () => {
expect(Bun.inspect(formData).length > 0).toBe(true);
});
+ describe("non-standard extensions", () => {
+ it("should support .length", () => {
+ const formData = new FormData();
+ formData.append("foo", "bar");
+ formData.append("foo", new Blob(["bar"]));
+ formData.append("bar", "baz");
+ // @ts-ignore
+ expect(formData.length).toBe(3);
+ formData.delete("foo");
+ // @ts-ignore
+ expect(formData.length).toBe(1);
+ formData.append("foo", "bar");
+ // @ts-ignore
+ expect(formData.length).toBe(2);
+ formData.delete("foo");
+ formData.delete("foo");
+ // @ts-ignore
+ expect(formData.length).toBe(1);
+ formData.delete("bar");
+ // @ts-ignore
+ expect(formData.length).toBe(0);
+ });
+ });
+
describe("URLEncoded", () => {
test("should parse URL encoded", async () => {
const response = new Response("foo=bar&baz=qux", {
diff --git a/test/js/web/html/URLSearchParams.test.ts b/test/js/web/html/URLSearchParams.test.ts
new file mode 100644
index 000000000..028590971
--- /dev/null
+++ b/test/js/web/html/URLSearchParams.test.ts
@@ -0,0 +1,63 @@
+import { describe, it, expect } from "bun:test";
+
+describe("URLSearchParams", () => {
+ describe("non-standard extensions", () => {
+ it("should support .length", () => {
+ const params = new URLSearchParams();
+ params.append("foo", "bar");
+ params.append("foo", "boop");
+ params.append("bar", "baz");
+ expect(params.length).toBe(3);
+ params.delete("foo");
+ expect(params.length).toBe(1);
+ params.append("foo", "bar");
+ expect(params.length).toBe(2);
+ params.delete("foo");
+ params.delete("foo");
+ expect(params.length).toBe(1);
+ params.delete("bar");
+ expect(params.length).toBe(0);
+ });
+
+ it("should support .toJSON", () => {
+ const params = new URLSearchParams();
+ params.append("foo", "bar");
+ params.append("foo", "boop");
+ params.append("bar", "baz");
+ // @ts-ignore
+ expect(params.toJSON()).toEqual({
+ foo: ["bar", "boop"],
+ bar: "baz",
+ });
+ expect(JSON.parse(JSON.stringify(params))).toEqual({
+ foo: ["bar", "boop"],
+ bar: "baz",
+ });
+ expect(Bun.inspect(params)).toBe(
+ "URLSearchParams {" + "\n" + ' foo: [ "bar", "boop" ],' + "\n" + ' bar: "baz"' + "\n" + "}",
+ );
+ params.delete("foo");
+ // @ts-ignore
+ expect(params.toJSON()).toEqual({
+ bar: "baz",
+ });
+ params.append("foo", "bar");
+ // @ts-ignore
+ expect(params.toJSON()).toEqual({
+ foo: "bar",
+ bar: "baz",
+ });
+ params.delete("foo");
+ params.delete("foo");
+ // @ts-ignore
+ expect(params.toJSON()).toEqual({
+ bar: "baz",
+ });
+ params.delete("bar");
+ // @ts-ignore
+ expect(params.toJSON()).toEqual({});
+
+ expect(JSON.stringify(params)).toBe("{}");
+ });
+ });
+});
diff --git a/test/js/web/url/url.test.ts b/test/js/web/url/url.test.ts
index 4e71c44d7..6ad691c1b 100644
--- a/test/js/web/url/url.test.ts
+++ b/test/js/web/url/url.test.ts
@@ -73,21 +73,7 @@ describe("url", () => {
pathname: "/",
hash: "",
search: "",
- searchParams: URLSearchParams {
- append: [Function: append],
- delete: [Function: delete],
- get: [Function: get],
- getAll: [Function: getAll],
- has: [Function: has],
- set: [Function: set],
- sort: [Function: sort],
- entries: [Function: entries],
- keys: [Function: keys],
- values: [Function: values],
- forEach: [Function: forEach],
- toString: [Function: toString],
- [Symbol(Symbol.iterator)]: [Function: entries]
- },
+ searchParams: ${Bun.inspect(new URLSearchParams())},
toJSON: [Function: toJSON],
toString: [Function: toString]
}`);
@@ -108,21 +94,7 @@ describe("url", () => {
pathname: "/oven-sh/bun/issues/135",
hash: "",
search: "?hello%20i%20have%20spaces%20thank%20you%20good%20night",
- searchParams: URLSearchParams {
- append: [Function: append],
- delete: [Function: delete],
- get: [Function: get],
- getAll: [Function: getAll],
- has: [Function: has],
- set: [Function: set],
- sort: [Function: sort],
- entries: [Function: entries],
- keys: [Function: keys],
- values: [Function: values],
- forEach: [Function: forEach],
- toString: [Function: toString],
- [Symbol(Symbol.iterator)]: [Function: entries]
- },
+ searchParams: URLSearchParams {\n \"hello i have spaces thank you good night\": \"\"\n },
toJSON: [Function: toJSON],
toString: [Function: toString]
}`);