import { describe, expect, it } from "bun:test";
import { normalizeBunSnapshot, tmpdirSync } from "harness";
import { join } from "path";
import util from "util";
it("prototype", () => {
const prototypes = [
Request.prototype,
Response.prototype,
Blob.prototype,
Headers.prototype,
URL.prototype,
URLSearchParams.prototype,
ReadableStream.prototype,
WritableStream.prototype,
TransformStream.prototype,
MessageEvent.prototype,
CloseEvent.prototype,
WebSocket.prototype,
];
for (let prototype of prototypes) {
for (let i = 0; i < 10; i++) expect(Bun.inspect(prototype).length > 0).toBeTrue();
}
Bun.gc(true);
});
it("getters", () => {
const obj = {
get foo() {
return 42;
},
};
expect(Bun.inspect(obj)).toBe("{\n" + " foo: [Getter]," + "\n" + "}");
var called = false;
const objWithThrowingGetter = {
get foo() {
called = true;
throw new Error("Test failed!");
},
set foo(v) {
called = true;
throw new Error("Test failed!");
},
};
expect(Bun.inspect(objWithThrowingGetter)).toBe("{\n" + " foo: [Getter/Setter]," + "\n" + "}");
expect(called).toBe(false);
});
it("setters", () => {
const obj = {
set foo(x) {},
};
expect(Bun.inspect(obj)).toBe("{\n" + " foo: [Setter]," + "\n" + "}");
var called = false;
const objWithThrowingGetter = {
get foo() {
called = true;
throw new Error("Test failed!");
},
set foo(v) {
called = true;
throw new Error("Test failed!");
},
};
expect(Bun.inspect(objWithThrowingGetter)).toBe("{\n" + " foo: [Getter/Setter]," + "\n" + "}");
expect(called).toBe(false);
});
it("getter/setters", () => {
const obj = {
get foo() {
return 42;
},
set foo(x) {},
};
expect(Bun.inspect(obj)).toBe("{\n" + " foo: [Getter/Setter]," + "\n" + "}");
});
it("Timeout", () => {
const id = setTimeout(() => {}, 0);
expect(Bun.inspect(id)).toBe(`Timeout (#${+id})`);
const id2 = setInterval(() => {}, 1);
id2.unref();
expect(Bun.inspect(id2)).toBe(`Timeout (#${+id2}, repeats)`);
});
it("when prototype defines the same property, don't print the same property twice", () => {
var base = {
foo: "123",
};
var obj = Object.create(base);
obj.foo = "456";
expect(Bun.inspect(obj).trim()).toBe('{\n foo: "456",\n}'.trim());
});
it("Blob inspect", () => {
expect(Bun.inspect(new Blob(["123"]))).toBe(`Blob (3 bytes)`);
expect(Bun.inspect(new Blob(["123".repeat(900)]))).toBe(`Blob (2.70 KB)`);
const tmpFile = join(tmpdirSync(), "file.txt");
expect(Bun.inspect(Bun.file(tmpFile))).toBe(`FileRef ("${tmpFile}") {
type: "text/plain;charset=utf-8"
}`);
expect(Bun.inspect(Bun.file(123))).toBe(`FileRef (fd: 123) {
type: "application/octet-stream"
}`);
expect(Bun.inspect(new Response(new Blob()))).toBe(`Response (0 KB) {
ok: true,
url: "",
status: 200,
statusText: "",
headers: Headers {},
redirected: false,
bodyUsed: false,
[Blob detached]
}`);
expect(Bun.inspect(new Response("Hello"))).toBe(`Response (5 bytes) {
ok: true,
url: "",
status: 200,
statusText: "",
headers: Headers {},
redirected: false,
bodyUsed: false,
Blob (5 bytes)
}`);
});
it("utf16 property name", () => {
var { Database } = require("bun:sqlite");
const db = Database.open(":memory:");
expect("笑".codePointAt(0)).toBe(31505);
// latin1 escaping identifier issue
expect(Object.keys({ 笑: "hey" })[0].codePointAt(0)).toBe(31505);
const output = Bun.inspect(
[
{
笑: "😀",
},
],
2,
);
expect(Bun.inspect(db.prepare("select '😀' as 笑").all())).toBe(output);
});
it("latin1", () => {
expect(Bun.inspect("English")).toBe('"English"');
expect(Bun.inspect("Français")).toBe('"Français"');
expect(Bun.inspect("Ελληνική")).toBe('"Ελληνική"');
expect(Bun.inspect("日本語")).toBe('"日本語"');
expect(Bun.inspect("Emoji😎")).toBe('"Emoji😎"');
expect(Bun.inspect("Français / Ελληνική")).toBe('"Français / Ελληνική"');
});
it("Request object", () => {
expect(Bun.inspect(new Request({ url: "https://example.com" })).trim()).toBe(
`
Request (0 KB) {
method: "GET",
url: "https://example.com/",
headers: Headers {}
}`.trim(),
);
});
it("MessageEvent", () => {
expect(Bun.inspect(new MessageEvent("message", { data: 123 }))).toBe(
`MessageEvent {
type: "message",
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,
}`,
);
});
// https://github.com/oven-sh/bun/issues/561
it("TypedArray prints", () => {
for (let TypedArray of [
Uint8Array,
Uint16Array,
Uint32Array,
Uint8ClampedArray,
Int8Array,
Int16Array,
Int32Array,
Float32Array,
Float64Array,
]) {
const buffer = new TypedArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
const input = Bun.inspect(buffer);
expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]`);
for (let i = 1; i < buffer.length + 1; i++) {
expect(Bun.inspect(buffer.subarray(i))).toBe(
buffer.length - i === 0
? `${TypedArray.name}(${buffer.length - i}) []`
: `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]",
);
}
}
});
it("BigIntArray", () => {
for (let TypedArray of [BigInt64Array, BigUint64Array]) {
const buffer = new TypedArray([1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n]);
const input = Bun.inspect(buffer);
expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ 1n, 2n, 3n, 4n, 5n, 6n, 7n, 8n, 9n, 10n ]`);
for (let i = 1; i < buffer.length + 1; i++) {
expect(Bun.inspect(buffer.subarray(i))).toBe(
buffer.length - i === 0
? `${TypedArray.name}(${buffer.length - i}) []`
: `${TypedArray.name}(${buffer.length - i}) [ ` +
[...buffer.subarray(i)].map(a => a.toString(10) + "n").join(", ") +
" ]",
);
}
}
});
for (let TypedArray of [Float32Array, Float64Array]) {
it(TypedArray.name + " " + Math.fround(42.68), () => {
const buffer = new TypedArray([Math.fround(42.68)]);
const input = Bun.inspect(buffer);
expect(input).toBe(`${TypedArray.name}(${buffer.length}) [ ${[Math.fround(42.68)].join(", ")} ]`);
for (let i = 1; i < buffer.length + 1; i++) {
expect(Bun.inspect(buffer.subarray(i))).toBe(
buffer.length - i === 0
? `${TypedArray.name}(${buffer.length - i}) []`
: `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]",
);
}
});
it(TypedArray.name + " " + 42.68, () => {
const buffer = new TypedArray([42.68]);
const input = Bun.inspect(buffer);
expect(input).toBe(
`${TypedArray.name}(${buffer.length}) [ ${[TypedArray === Float32Array ? Math.fround(42.68) : 42.68].join(", ")} ]`,
);
for (let i = 1; i < buffer.length + 1; i++) {
expect(Bun.inspect(buffer.subarray(i))).toBe(
buffer.length - i === 0
? `${TypedArray.name}(${buffer.length - i}) []`
: `${TypedArray.name}(${buffer.length - i}) [ ` + [...buffer.subarray(i)].join(", ") + " ]",
);
}
});
}
it("jsx with two elements", () => {
const input = Bun.inspect(
string inside child
,
);
const output = `
string inside child
`;
expect(input).toBe(output);
});
const Foo = () => foo
;
it("jsx with anon component", () => {
const input = Bun.inspect();
const output = ``;
expect(input).toBe(output);
});
it("jsx with fragment", () => {
const input = Bun.inspect(<>foo bar>);
const output = `<>foo bar>`;
expect(input).toBe(output);
});
it("inspect", () => {
expect(Bun.inspect(new TypeError("what")).includes("TypeError: what")).toBe(true);
expect(Bun.inspect("hi")).toBe('"hi"');
expect(Bun.inspect(1)).toBe("1");
expect(Bun.inspect(NaN)).toBe("NaN");
expect(Bun.inspect(Infinity)).toBe("Infinity");
expect(Bun.inspect(-Infinity)).toBe("-Infinity");
expect(Bun.inspect([])).toBe("[]");
expect(Bun.inspect({})).toBe("{}");
expect(Bun.inspect({ hello: 1 })).toBe("{\n hello: 1,\n}");
expect(Bun.inspect({ hello: 1, there: 2 })).toBe("{\n hello: 1,\n there: 2,\n}");
expect(Bun.inspect({ hello: "1", there: 2 })).toBe('{\n hello: "1",\n there: 2,\n}');
expect(Bun.inspect({ 'hello-"there': "1", there: 2 })).toBe('{\n "hello-\\"there": "1",\n there: 2,\n}');
var str = "123";
while (str.length < 4096) {
str += "123";
}
expect(Bun.inspect(str)).toBe('"' + str + '"');
// expect(Bun.inspect(new Headers())).toBe("Headers (0 KB) {}");
expect(Bun.inspect(new Response()).length > 0).toBe(true);
// expect(
// JSON.stringify(
// new Headers({
// hi: "ok",
// })
// )
// ).toBe('{"hi":"ok"}');
expect(Bun.inspect(new Set())).toBe("Set {}");
expect(Bun.inspect(new Map())).toBe("Map {}");
expect(Bun.inspect(new Map([["foo", "bar"]]))).toBe('Map(1) {\n "foo": "bar",\n}');
expect(Bun.inspect(new Set(["bar"]))).toBe('Set(1) {\n "bar",\n}');
// Regression test: Set/Map with overridden size property should not panic
const setWithOverriddenSize = new Set();
Object.defineProperty(setWithOverriddenSize, "size", {
writable: true,
enumerable: true,
value: Set,
});
expect(Bun.inspect(setWithOverriddenSize)).toBe("Set {}");
const mapWithOverriddenSize = new Map();
Object.defineProperty(mapWithOverriddenSize, "size", {
writable: true,
enumerable: true,
value: "not a number",
});
expect(Bun.inspect(mapWithOverriddenSize)).toBe("Map {}");
expect(Bun.inspect(foo
)).toBe("foo
");
expect(Bun.inspect(foo
)).toBe("foo
");
expect(Bun.inspect(foo
)).toBe("foo
");
expect(Bun.inspect(hi
)).toBe("hi
");
expect(Bun.inspect(quoted
)).toBe('quoted
');
expect(
Bun.inspect(
,
),
).toBe(
`
`.trim(),
);
expect(Bun.inspect(BigInt(32))).toBe("32n");
expect(Bun.inspect({ call: 1, not_call: 2, prototype: 4 })).toBe(
`
{
call: 1,
not_call: 2,
prototype: 4,
}
`.trim(),
);
});
describe("latin1 supplemental", () => {
const fixture = [
[["äbc"], '[ "äbc" ]'],
[["cbä"], '[ "cbä" ]'],
[["cäb"], '[ "cäb" ]'],
[["äbc äbc"], '[ "äbc äbc" ]'],
[["cbä cbä"], '[ "cbä cbä" ]'],
[["cäb cäb"], '[ "cäb cäb" ]'],
];
for (let [input, output] of fixture) {
it(`latin1 (input) \"${input}\" ${output}`, () => {
expect(Bun.inspect(input)).toBe(output);
});
}
// this test is failing:
it(`latin1 (property key)`, () => {
expect(
Object.keys({
ä: 1,
})[0].codePointAt(0),
).toBe(228);
});
});
const tmpdir = tmpdirSync();
const fixture = [
() => globalThis,
() => Bun.file(join(tmpdir, "log.txt")).stream(),
() => Bun.file(join(tmpdir, "log.1.txt")).stream().getReader(),
() => Bun.file(join(tmpdir, "log.2.txt")).writer(),
() =>
new WritableStream({
write(chunk) {},
}),
() => require("events"),
() => {
return new (import.meta.require("events").EventEmitter)();
},
async () => await import("node:assert"),
async () => await import("../../empty.js.js"),
() => import.meta.require("./empty.js"),
() => new Proxy({ yolo: 1 }, {}),
() =>
new Proxy(
{ yolo: 1 },
{
get(target, prop) {
return prop + "!";
},
has(target, prop) {
return true;
},
ownKeys() {
return ["foo"];
},
},
),
];
describe("crash testing", () => {
for (let input of fixture) {
it(`inspecting "${input.toString().slice(0, 20).replaceAll("\n", "\\n")}" doesn't crash`, async () => {
try {
console.log("asked" + input.toString().slice(0, 20).replaceAll("\n", "\\n"));
Bun.inspect(await input());
console.log("who");
} catch (e) {
// this can throw its fine
}
});
}
});
it("possibly formatted emojis log", () => {
expect(Bun.inspect("✔")).toBe('"✔"');
});
it("new Date(..)", () => {
let s = Bun.inspect(new Date(1679911059000 - new Date().getTimezoneOffset()));
expect(s).toContain("2023-03-27T");
expect(s).toHaveLength(24);
let offset = new Date().getTimezoneOffset() / 60;
let hour = (9 - offset).toString();
if (hour.length === 1) {
hour = "0" + hour;
}
expect(Bun.inspect(new Date("March 27, 2023 " + hour + ":54:00"))).toBe("2023-03-27T09:54:00.000Z");
expect(Bun.inspect(new Date("2023-03-27T" + hour + ":54:00"))).toBe("2023-03-27T09:54:00.000Z");
expect(Bun.inspect(new Date(2023, 2, 27, -offset))).toBe("2023-03-27T00:00:00.000Z");
expect(Bun.inspect(new Date(2023, 2, 27, 9 - offset, 54, 0))).toBe("2023-03-27T09:54:00.000Z");
expect(Bun.inspect(new Date("1679911059000"))).toBe("Invalid Date");
expect(Bun.inspect(new Date("hello world"))).toBe("Invalid Date");
expect(Bun.inspect(new Date("Invalid Date"))).toBe("Invalid Date");
});
it("Bun.inspect.custom exists", () => {
expect(Bun.inspect.custom).toBe(util.inspect.custom);
});
describe("Functions with names", () => {
const closures = [
() => function f() {},
() => {
var f = function () {};
return f;
},
() => {
const f = function () {};
// workaround transpiler inlining losing the display name
// TODO: preserve the name on functions being inlined
f.length;
return f;
},
() => {
let f = function () {};
// workaround transpiler inlining losing the display name
// TODO: preserve the name on functions being inlined
f.length;
return f;
},
() => {
var f = function f() {};
return f;
},
() => {
var foo = function f() {};
return foo;
},
() => {
function f() {}
var foo = f;
return foo;
},
];
for (let closure of closures) {
it(JSON.stringify(closure.toString()), () => {
expect(Bun.inspect(closure())).toBe("[Function: f]");
});
}
});
it("Bun.inspect array with non-indexed properties", () => {
const a = [1, 2, 3];
a.length = 42;
a[18] = 24;
a.potato = "hello";
console.log(a);
expect(Bun.inspect(a)).toBe(`[
1, 2, 3, 15 x empty items, 24, 23 x empty items, potato: "hello"
]`);
});
describe("console.logging function displays async and generator names", async () => {
const cases = [
function () {},
function a() {},
async function b() {},
function* c() {},
async function* d() {},
async function* () {},
];
const expected_logs = [
"[Function]",
"[Function: a]",
"[AsyncFunction: b]",
"[GeneratorFunction: c]",
"[AsyncGeneratorFunction: d]",
"[AsyncGeneratorFunction]",
];
for (let i = 0; i < cases.length; i++) {
it(expected_logs[i], () => {
expect(Bun.inspect(cases[i])).toBe(expected_logs[i]);
});
}
});
describe("console.logging class displays names and extends", async () => {
class A {}
const cases = [A, class B extends A {}, class extends A {}, class {}];
const expected_logs = ["[class A]", "[class B extends A]", "[class (anonymous) extends A]", "[class (anonymous)]"];
for (let i = 0; i < cases.length; i++) {
it(expected_logs[i], () => {
expect(Bun.inspect(cases[i])).toBe(expected_logs[i]);
});
}
});
it("console.log on a Blob shows name", () => {
const blob = new Blob(["foo"], { type: "text/plain" });
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n type: "text/plain;charset=utf-8"\n}');
blob.name = "bar";
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n name: "bar",\n type: "text/plain;charset=utf-8"\n}');
blob.name = "foobar";
expect(Bun.inspect(blob)).toBe('Blob (3 bytes) {\n name: "foobar",\n type: "text/plain;charset=utf-8"\n}');
const file = new File(["foo"], "bar.txt", { type: "text/plain" });
expect(Bun.inspect(file)).toBe(
`File (3 bytes) {\n name: "bar.txt",\n type: "text/plain;charset=utf-8",\n lastModified: ${file.lastModified}\n}`,
);
file.name = "foobar";
expect(Bun.inspect(file)).toBe(
`File (3 bytes) {\n name: "foobar",\n type: "text/plain;charset=utf-8",\n lastModified: ${file.lastModified}\n}`,
);
file.name = "";
expect(Bun.inspect(file)).toBe(
`File (3 bytes) {\n name: "",\n type: "text/plain;charset=utf-8",\n lastModified: ${file.lastModified}\n}`,
);
});
it("console.log on a arguments shows list", () => {
function fn() {
expect(Bun.inspect(arguments)).toBe(`[ 1, [ 1 ], [Function: fn] ]`);
}
fn(1, [1], fn);
});
it("console.log on null prototype", () => {
expect(Bun.inspect(Object.create(null))).toBe("[Object: null prototype] {}");
});
it("Symbol", () => {
expect(Bun.inspect(Symbol())).toBe("Symbol()");
expect(Bun.inspect(Symbol(""))).toBe("Symbol()");
});
it("CloseEvent", () => {
const closeEvent = new CloseEvent("close", {
code: 1000,
reason: "Normal",
});
expect(Bun.inspect(closeEvent)).toMatchInlineSnapshot(`
"CloseEvent {
isTrusted: false,
wasClean: false,
code: 1000,
reason: "Normal",
type: "close",
target: null,
currentTarget: null,
eventPhase: 0,
cancelBubble: false,
bubbles: false,
cancelable: false,
defaultPrevented: false,
composed: false,
timeStamp: 0,
srcElement: null,
returnValue: true,
composedPath: [Function: composedPath],
stopPropagation: [Function: stopPropagation],
stopImmediatePropagation: [Function: stopImmediatePropagation],
preventDefault: [Function: preventDefault],
initEvent: [Function: initEvent],
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3,
}"
`);
});
it("ErrorEvent", () => {
const errorEvent = new ErrorEvent("error", {
message: "Something went wrong",
filename: "script.js",
lineno: 42,
colno: 10,
error: new Error("Test error"),
});
expect(normalizeBunSnapshot(Bun.inspect(errorEvent)).replace(/\d+ \| /gim, "NNN |")).toMatchInlineSnapshot(`
"ErrorEvent {
type: "error",
message: "Something went wrong",
error: NNN | const errorEvent = new ErrorEvent("error", {
NNN | message: "Something went wrong",
NNN | filename: "script.js",
NNN | lineno: 42,
NNN | colno: 10,
NNN | error: new Error("Test error"),
^
error: Test error
at (file:NN:NN)
,
}"
`);
});
it("MessageEvent", () => {
const messageEvent = new MessageEvent("message", {
data: "Hello, world!",
origin: "https://example.com",
lastEventId: "123",
source: null,
ports: [],
});
expect(Bun.inspect(messageEvent)).toMatchInlineSnapshot(`
"MessageEvent {
type: "message",
data: "Hello, world!",
}"
`);
});
it("CustomEvent", () => {
const customEvent = new CustomEvent("custom", {
detail: { value: 42, name: "test" },
bubbles: true,
cancelable: true,
});
expect(Bun.inspect(customEvent)).toMatchInlineSnapshot(`
"CustomEvent {
isTrusted: false,
detail: {
value: 42,
name: "test",
},
initCustomEvent: [Function: initCustomEvent],
type: "custom",
target: null,
currentTarget: null,
eventPhase: 0,
cancelBubble: false,
bubbles: true,
cancelable: true,
defaultPrevented: false,
composed: false,
timeStamp: 0,
srcElement: null,
returnValue: true,
composedPath: [Function: composedPath],
stopPropagation: [Function: stopPropagation],
stopImmediatePropagation: [Function: stopImmediatePropagation],
preventDefault: [Function: preventDefault],
initEvent: [Function: initEvent],
NONE: 0,
CAPTURING_PHASE: 1,
AT_TARGET: 2,
BUBBLING_PHASE: 3,
}"
`);
});