diff --git a/src/runtime/test_runner/pretty_format.rs b/src/runtime/test_runner/pretty_format.rs index 05f3b81ab0e7..6dfa504bda95 100644 --- a/src/runtime/test_runner/pretty_format.rs +++ b/src/runtime/test_runner/pretty_format.rs @@ -1582,20 +1582,27 @@ impl<'a> Formatter<'a> { } return Ok(()); } else if bun_jsc::DOMFormData::from_js(value).is_some() { - let to_json_function = value.get(self.global_this, "toJSON")?.unwrap(); + if let Some(to_json_function) = value + .get(self.global_this, "toJSON")? + .filter(|f| f.is_callable()) + { + self.add_for_new_line(b"FormData (entries) ".len()); + writer.write_all( + pretty_fmt_const::( + "FormData (entries) ", + ) + .as_bytes(), + ); - self.add_for_new_line(b"FormData (entries) ".len()); - writer.write_all( - pretty_fmt_const::( - "FormData (entries) ", - ) - .as_bytes(), - ); + return self.print_as::( + writer.ctx, + to_json_function.call(self.global_this, value, &[])?, + JSType::Object, + ); + } return self.print_as::( - writer.ctx, - to_json_function.call(self.global_this, value, &[])?, - JSType::Object, + writer.ctx, value, JSType::Event, ); } else if let Some(timer) = value.as_class_ref::() { self.add_for_new_line( diff --git a/test/js/bun/test/expect-formdata-tojson-crash.test.ts b/test/js/bun/test/expect-formdata-tojson-crash.test.ts new file mode 100644 index 000000000000..7e3e86462c57 --- /dev/null +++ b/test/js/bun/test/expect-formdata-tojson-crash.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, tempDir } from "harness"; + +test("failing matcher on FormData without a callable toJSON does not abort the test runner", async () => { + using dir = tempDir("formdata-tojson-crash", { + "formdata.test.ts": ` + import { test, expect } from "bun:test"; + + test("toJSON shadowed with undefined", () => { + const fd = new FormData(); + Object.defineProperty(fd, "toJSON", { value: undefined }); + expect(fd).toEqual(1 as any); + }); + + test("null prototype", () => { + const fd = new FormData(); + fd.append("a", "b"); + Object.setPrototypeOf(fd, null); + expect(fd).toEqual(1 as any); + }); + + test("toJSON is a non-callable value", () => { + const fd = new FormData(); + fd.append("a", "b"); + Object.defineProperty(fd, "toJSON", { value: 42 }); + expect(fd).toEqual(1 as any); + }); + `, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "test", "formdata.test.ts"], + env: bunEnv, + cwd: String(dir), + stdout: "pipe", + stderr: "pipe", + }); + + const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stderr).toContain("Received: FormData"); + // The non-callable toJSON is printed as a property rather than being called. + expect(stderr).toContain(`"toJSON": 42`); + expect(stderr).not.toContain("is not a function"); + expect(stderr).toContain("3 fail"); + expect(exitCode).toBe(1); +});