diff --git a/src/bun.js/ConsoleObject.zig b/src/bun.js/ConsoleObject.zig index 587ef28799..ae585561ac 100644 --- a/src/bun.js/ConsoleObject.zig +++ b/src/bun.js/ConsoleObject.zig @@ -2599,10 +2599,21 @@ pub const Formatter = struct { writer.print("{}", .{str}); }, .Event => { - const event_type = EventType.map.getWithEql(value.get(this.globalThis, "type").?.getZigString(this.globalThis), ZigString.eqlComptime) orelse EventType.unknown; - if (event_type != .MessageEvent and event_type != .ErrorEvent) { - return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); - } + const event_type_value = brk: { + const value_ = value.get(this.globalThis, "type") orelse break :brk JSValue.undefined; + if (value_.isString()) { + break :brk value_; + } + + break :brk JSValue.undefined; + }; + + const event_type = switch (EventType.map.fromJS(this.globalThis, event_type_value) orelse .unknown) { + .MessageEvent, .ErrorEvent => |evt| evt, + else => { + return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + }, + }; writer.print( comptime Output.prettyFmt("{s} {{\n", enable_ansi_colors), @@ -2628,57 +2639,68 @@ pub const Formatter = struct { event_type.label(), }, ); - if (!single_line) { + }, + } + + if (value.fastGet(this.globalThis, .message)) |message_value| { + if (message_value.isString()) { + if (!this.single_line) { this.writeIndent(Writer, writer_) catch unreachable; } - }, + + writer.print( + comptime Output.prettyFmt("message: ", enable_ansi_colors), + .{}, + ); + + const tag = Tag.getAdvanced(message_value, this.globalThis, .{ .hide_global = true }); + this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + if (!this.single_line) { + writer.writeAll("\n"); + } + } } switch (event_type) { .MessageEvent => { + if (!this.single_line) { + this.writeIndent(Writer, writer_) catch unreachable; + } + writer.print( comptime Output.prettyFmt("data: ", enable_ansi_colors), .{}, ); - const data = value.fastGet(this.globalThis, .data).?; + const data = value.fastGet(this.globalThis, .data) orelse JSValue.undefined; const tag = Tag.getAdvanced(data, this.globalThis, .{ .hide_global = true }); - if (tag.cell.isStringLike()) { - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); - } else { - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + if (!this.single_line) { + writer.writeAll("\n"); } }, .ErrorEvent => { - { - const error_value = value.get(this.globalThis, "error").?; - - if (!error_value.isEmptyOrUndefinedOrNull()) { - writer.print( - comptime Output.prettyFmt("error: ", enable_ansi_colors), - .{}, - ); - - const tag = Tag.getAdvanced(error_value, this.globalThis, .{ .hide_global = true }); - this.format(tag, Writer, writer_, error_value, this.globalThis, enable_ansi_colors); + if (value.fastGet(this.globalThis, .@"error")) |error_value| { + if (!this.single_line) { + this.writeIndent(Writer, writer_) catch unreachable; } - } - const message_value = value.get(this.globalThis, "message").?; - if (message_value.isString()) { writer.print( - comptime Output.prettyFmt("message: ", enable_ansi_colors), + comptime Output.prettyFmt("error: ", enable_ansi_colors), .{}, ); - const tag = Tag.getAdvanced(message_value, this.globalThis, .{ .hide_global = true }); - this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); + const tag = Tag.getAdvanced(error_value, this.globalThis, .{ .hide_global = true }); + this.format(tag, Writer, writer_, error_value, this.globalThis, enable_ansi_colors); + this.printComma(Writer, writer_, enable_ansi_colors) catch unreachable; + if (!this.single_line) { + writer.writeAll("\n"); + } } }, else => unreachable, } - if (!this.single_line) { - writer.writeAll("\n"); - } } if (!this.single_line) { diff --git a/src/bun.js/api/server.zig b/src/bun.js/api/server.zig index 6d8e50568f..b09c3797f2 100644 --- a/src/bun.js/api/server.zig +++ b/src/bun.js/api/server.zig @@ -1001,7 +1001,7 @@ pub const ServerConfig = struct { } } - if (arg.getTruthy(global, "error")) |onError| { + if (arg.getTruthyComptime(global, "error")) |onError| { if (!onError.isCallable(global.vm())) { JSC.throwInvalidArguments("Expected error to be a function", .{}, global, exception); if (args.ssl_config) |*conf| { @@ -3667,7 +3667,7 @@ pub const WebSocketServer = struct { var valid = false; - if (object.getTruthy(globalObject, "message")) |message_| { + if (object.getTruthyComptime(globalObject, "message")) |message_| { if (!message_.isCallable(vm)) { globalObject.throwInvalidArguments("websocket expects a function for the message option", .{}); return null; diff --git a/src/bun.js/bindings/bindings.cpp b/src/bun.js/bindings/bindings.cpp index 389a049535..c99ff1f095 100644 --- a/src/bun.js/bindings/bindings.cpp +++ b/src/bun.js/bindings/bindings.cpp @@ -4814,9 +4814,12 @@ enum class BuiltinNamesMap : uint8_t { path, stream, asyncIterator, + name, + message, + error, }; -static JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigned char name) +static const JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigned char name) { auto& vm = globalObject->vm(); auto clientData = WebCore::clientData(vm); @@ -4863,6 +4866,15 @@ static JSC::Identifier builtinNameMap(JSC::JSGlobalObject* globalObject, unsigne case BuiltinNamesMap::asyncIterator: { return vm.propertyNames->asyncIteratorSymbol; } + case BuiltinNamesMap::name: { + return vm.propertyNames->name; + } + case BuiltinNamesMap::message: { + return vm.propertyNames->message; + } + case BuiltinNamesMap::error: { + return vm.propertyNames->error; + } default: { ASSERT_NOT_REACHED(); return Identifier(); diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index 99b74e577e..5fac4fb09c 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -4828,6 +4828,9 @@ pub const JSValue = enum(JSValueReprInt) { path, stream, asyncIterator, + name, + message, + @"error", pub fn has(property: []const u8) bool { return bun.ComptimeEnumMap(BuiltinName).has(property); diff --git a/src/bun.js/bindings/webcore/WebSocket.cpp b/src/bun.js/bindings/webcore/WebSocket.cpp index 2358056059..d535e6df42 100644 --- a/src/bun.js/bindings/webcore/WebSocket.cpp +++ b/src/bun.js/bindings/webcore/WebSocket.cpp @@ -1155,7 +1155,14 @@ void WebSocket::didReceiveClose(CleanStatus wasClean, unsigned short code, WTF:: if (auto* context = scriptExecutionContext()) { this->incPendingActivityCount(); if (wasConnecting && isConnectionError) { - dispatchEvent(Event::create(eventNames().errorEvent, Event::CanBubble::No, Event::IsCancelable::No)); + ErrorEvent::Init eventInit = {}; + eventInit.message = makeString("WebSocket connection to '"_s, m_url.stringCenterEllipsizedToLength(), "' failed: "_s, reason); + eventInit.filename = String(); + eventInit.bubbles = false; + eventInit.cancelable = false; + eventInit.colno = 0; + eventInit.error = {}; + dispatchEvent(ErrorEvent::create(eventNames().errorEvent, eventInit, EventIsTrusted::Yes)); } // https://html.spec.whatwg.org/multipage/web-sockets.html#feedback-from-the-protocol:concept-websocket-closed, we should synchronously fire a close event. dispatchEvent(CloseEvent::create(wasClean == CleanStatus::Clean, code, reason)); diff --git a/src/bun.js/test/expect.zig b/src/bun.js/test/expect.zig index 0c2dba92d6..dc23e1146c 100644 --- a/src/bun.js/test/expect.zig +++ b/src/bun.js/test/expect.zig @@ -2344,8 +2344,8 @@ pub const Expect = struct { if (expected_value.isEmpty() or expected_value.isUndefined()) { const signature_no_args = comptime getSignature("toThrow", "", true); if (result.toError()) |err| { - const name = err.get(globalThis, "name") orelse JSValue.undefined; - const message = err.get(globalThis, "message") orelse JSValue.undefined; + const name = err.getTruthyComptime(globalThis, "name") orelse JSValue.undefined; + const message = err.getTruthyComptime(globalThis, "message") orelse JSValue.undefined; const fmt = signature_no_args ++ "\n\nError name: {any}\nError message: {any}\n"; globalThis.throwPretty(fmt, .{ name.toFmt(globalThis, &formatter), @@ -2491,7 +2491,7 @@ pub const Expect = struct { // If it's not an object, we are going to crash here. assert(expected_value.isObject()); - if (expected_value.get(globalThis, "message")) |expected_message| { + if (expected_value.fastGet(globalThis, .message)) |expected_message| { const signature = comptime getSignature("toThrow", "expected", false); if (_received_message) |received_message| { @@ -4629,7 +4629,7 @@ pub const Expect = struct { pass = pass_value.toBooleanSlow(globalThis); if (globalThis.hasException()) return false; - if (result.get(globalThis, "message")) |message_value| { + if (result.fastGet(globalThis, .message)) |message_value| { if (!message_value.isString() and !message_value.isCallable(globalThis.vm())) { break :valid false; } diff --git a/src/bun.js/test/pretty_format.zig b/src/bun.js/test/pretty_format.zig index 7f027b5b07..66f5cdd5a1 100644 --- a/src/bun.js/test/pretty_format.zig +++ b/src/bun.js/test/pretty_format.zig @@ -1101,11 +1101,14 @@ pub const JestPrettyFormat = struct { .Error => { var classname = ZigString.Empty; value.getClassName(this.globalThis, &classname); - var message_string = ZigString.Empty; - if (value.get(this.globalThis, "message")) |message_prop| { - message_prop.toZigString(&message_string, this.globalThis); + var message_string = bun.String.empty; + defer message_string.deref(); + + if (value.fastGet(this.globalThis, .message)) |message_prop| { + message_string = message_prop.toBunString(this.globalThis); } - if (message_string.len == 0) { + + if (message_string.isEmpty()) { writer.print("[{s}]", .{classname}); return; } @@ -1384,10 +1387,21 @@ pub const JestPrettyFormat = struct { writer.print("{}", .{str}); }, .Event => { - const event_type = EventType.map.getWithEql(value.get(this.globalThis, "type").?.getZigString(this.globalThis), ZigString.eqlComptime) orelse EventType.unknown; - if (event_type != .MessageEvent and event_type != .ErrorEvent) { - return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); - } + const event_type_value = brk: { + const value_ = value.get(this.globalThis, "type") orelse break :brk JSValue.undefined; + if (value_.isString()) { + break :brk value_; + } + + break :brk JSValue.undefined; + }; + + const event_type = switch (EventType.map.fromJS(this.globalThis, event_type_value) orelse .unknown) { + .MessageEvent, .ErrorEvent => |evt| evt, + else => { + return this.printAs(.Object, Writer, writer_, value, .Event, enable_ansi_colors); + }, + }; writer.print( comptime Output.prettyFmt("{s} {{\n", enable_ansi_colors), @@ -1409,35 +1423,53 @@ pub const JestPrettyFormat = struct { event_type.label(), }, ); - this.writeIndent(Writer, writer_) catch unreachable; + + if (value.fastGet(this.globalThis, .message)) |message_value| { + if (message_value.isString()) { + this.writeIndent(Writer, writer_) catch unreachable; + writer.print( + comptime Output.prettyFmt("message: ", enable_ansi_colors), + .{}, + ); + + const tag = Tag.get(message_value, this.globalThis); + this.format(tag, Writer, writer_, message_value, this.globalThis, enable_ansi_colors); + writer.writeAll(", \n"); + } + } switch (event_type) { .MessageEvent => { + this.writeIndent(Writer, writer_) catch unreachable; writer.print( comptime Output.prettyFmt("data: ", enable_ansi_colors), .{}, ); - const data = value.fastGet(this.globalThis, .data).?; + const data = value.fastGet(this.globalThis, .data) orelse JSValue.undefined; const tag = Tag.get(data, this.globalThis); + if (tag.cell.isStringLike()) { this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); } else { this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); } + writer.writeAll(", \n"); }, .ErrorEvent => { - writer.print( - comptime Output.prettyFmt("error:\n", enable_ansi_colors), - .{}, - ); + if (value.fastGet(this.globalThis, .@"error")) |data| { + this.writeIndent(Writer, writer_) catch unreachable; + writer.print( + comptime Output.prettyFmt("error: ", enable_ansi_colors), + .{}, + ); - const data = value.get(this.globalThis, "error").?; - const tag = Tag.get(data, this.globalThis); - this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + const tag = Tag.get(data, this.globalThis); + this.format(tag, Writer, writer_, data, this.globalThis, enable_ansi_colors); + writer.writeAll("\n"); + } }, else => unreachable, } - writer.writeAll("\n"); } this.writeIndent(Writer, writer_) catch unreachable; diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 5360f3f051..1f9dd6e126 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -174,7 +174,32 @@ it("MessageEvent", () => { expect(Bun.inspect(new MessageEvent("message", { data: 123 }))).toBe( `MessageEvent { type: "message", - data: 123 + data: 123, +}`, + ); +}); + +it("MessageEvent with no data set", () => { + expect(Bun.inspect(new MessageEvent("message"))).toBe( + `MessageEvent { + type: "message", + data: null, +}`, + ); +}); + +it("MessageEvent with deleted data", () => { + const event = new MessageEvent("message"); + Object.defineProperty(event, "data", { + value: 123, + writable: true, + configurable: true, + }); + delete event.data; + expect(Bun.inspect(event)).toBe( + `MessageEvent { + type: "message", + data: null, }`, ); }); diff --git a/test/js/web/websocket/__snapshots__/error-event.test.ts.snap b/test/js/web/websocket/__snapshots__/error-event.test.ts.snap new file mode 100644 index 0000000000..62c8d485e2 --- /dev/null +++ b/test/js/web/websocket/__snapshots__/error-event.test.ts.snap @@ -0,0 +1,29 @@ +// Bun Snapshot v1, https://goo.gl/fbAQLP + +exports[`WebSocket error event snapshot: Snapshot snapshot 1`] = `ErrorEvent { + type: "error", + message: "WebSocket connection to 'ws://127.0.0.1:8080/' failed: Failed to connect", + error: null +}`; + +exports[`WebSocket error event snapshot: Inspect snapshot 1`] = ` +"ErrorEvent { + type: "error", + message: "WebSocket connection to 'ws://127.0.0.1:8080/' failed: Failed to connect", + error: null, +}" +`; + +exports[`ErrorEvent with no message: Inspect snapshot 1`] = ` +"ErrorEvent { + type: "error", + message: "", + error: null, +}" +`; + +exports[`ErrorEvent with no message: Snapshot snapshot 1`] = `ErrorEvent { + type: "error", + message: "", + error: null +}`; diff --git a/test/js/web/websocket/error-event.test.ts b/test/js/web/websocket/error-event.test.ts new file mode 100644 index 0000000000..ebc238d0db --- /dev/null +++ b/test/js/web/websocket/error-event.test.ts @@ -0,0 +1,19 @@ +import { test, expect } from "bun:test"; + +test("WebSocket error event snapshot", async () => { + const ws = new WebSocket("ws://127.0.0.1:8080"); + const { promise, resolve } = Promise.withResolvers(); + ws.onerror = error => { + resolve(error); + }; + const error = await promise; + expect(error).toMatchSnapshot("Snapshot snapshot"); + expect(Bun.inspect(error)).toMatchSnapshot("Inspect snapshot"); +}); + +test("ErrorEvent with no message", async () => { + const error = new ErrorEvent("error"); + expect(error.message).toBe(""); + expect(Bun.inspect(error)).toMatchSnapshot("Inspect snapshot"); + expect(error).toMatchSnapshot("Snapshot snapshot"); +});