Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions src/runtime/test_runner/pretty_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,20 +492,22 @@ impl Tag {
return Tag::get(value.get_proxy_target(), global_this);
}

// Is this a react element?
// Is this a react element? Keep the list in sync with `ConsoleObject.rs`.
if js_type.is_object() && js_type != JSType::ProxyObject {
if let Some(typeof_symbol) = value.get_own_truthy(global_this, "$$typeof")? {
let mut react_element = ZigString::init(b"react.element");
let mut react_fragment = ZigString::init(b"react.fragment");

if typeof_symbol
.is_same_value(JSValue::symbol_for(global_this, &mut react_element), global_this)?
|| typeof_symbol.is_same_value(
JSValue::symbol_for(global_this, &mut react_fragment),
global_this,
)?
{
return Ok(TagResult { tag: Tag::JSX, cell: js_type });
const REACT_ELEMENT_SYMBOLS: [&[u8]; 3] = [
b"react.element",
// React 19 - https://github.com/oven-sh/bun/issues/17223
b"react.transitional.element",
b"react.fragment",
];

for symbol_key in REACT_ELEMENT_SYMBOLS {
let mut symbol_key = ZigString::init(symbol_key);
let react_symbol = JSValue::symbol_for(global_this, &mut symbol_key);
if typeof_symbol.is_same_value(react_symbol, global_this)? {
return Ok(TagResult { tag: Tag::JSX, cell: js_type });
}
}
}
}
Expand Down
49 changes: 49 additions & 0 deletions test/js/bun/test/snapshot-tests/snapshots/snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,55 @@ test("basic unchanging inline snapshot", () => {
);
});

// React 18 marks elements with Symbol.for("react.element"), React 19 with
// Symbol.for("react.transitional.element"). Both serialize as JSX, the way React 18
// elements always have, instead of as the element's fields. test/ pins React 18, so
// the elements are built by hand.
describe.each(["react.element", "react.transitional.element"])("inline snapshots of %s elements", $$typeofKey => {
const $$typeof = Symbol.for($$typeofKey);
const h = (type: unknown, props: Record<string, unknown>, key: string | null = null) => ({
$$typeof,
type,
key,
ref: null,
props,
});
function Greeting() {}

test("serialize as JSX", () => {
expect(h("div", { id: "x" })).toMatchInlineSnapshot(`<div id="x" />`);
expect(h("li", { children: "one" }, "a")).toMatchInlineSnapshot(`<li key="a">one</li>`);
expect(h(Greeting, { name: "bun" })).toMatchInlineSnapshot(`<Greeting name="bun" />`);
});

test("child elements serialize as JSX", () => {
// Nested in an object so this only covers how the elements and their children are
// classified, not how a multi-line top-level value is wrapped.
expect({
one: h("ul", { className: "list", children: h("li", { children: "one" }) }),
many: h("ul", { children: [h("li", { children: "one" }, "1"), h("li", { children: "two" }, "2")] }),
}).toMatchInlineSnapshot(`
{
"many": <ul>
<li key="1">one</li>
<li key="2">two</li>
</ul>,
"one": <ul className="list">
<li>one</li>
</ul>,
}
`);
});

test("elements inside an array serialize as JSX", () => {
expect([h("br", {})]).toMatchInlineSnapshot(`
[
<br />,
]
`);
});
});

class InlineSnapshotTester {
tmpdir: string;
tmpid: number;
Expand Down
58 changes: 58 additions & 0 deletions test/js/bun/test/test-test.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,64 @@ it("expect().toEqual() on objects with property indices doesn't print undefined"
expect(err).not.toContain("undefined");
});

// React 18 marks elements with Symbol.for("react.element"), React 19 with
// Symbol.for("react.transitional.element"). Matcher diffs print both as JSX, the way
// React 18 elements always have, instead of dumping the element's fields.
it.each(["react.element", "react.transitional.element"])(
"expect() diffs print %s elements as JSX",
async $$typeofKey => {
using dir = tempDir("diff-jsx-" + $$typeofKey, {
"jsx.test.js": `
import { expect, test } from "bun:test";
const $$typeof = Symbol.for(${JSON.stringify($$typeofKey)});
const h = (type, props) => ({ $$typeof, type, key: null, ref: null, props });
test("toEqual", () => {
expect(h("div", { id: "received" })).toEqual(h("div", { id: "expected" }));
});
test("toStrictEqual with child elements", () => {
expect(h("ul", { children: [h("li", { children: "one" })] })).toStrictEqual(
h("ul", { children: [h("li", { children: "two" })] }),
);
});
test("not.toEqual", () => {
const el = h("div", { id: "x" });
expect(el).not.toEqual(el);
});
`,
});
await using proc = spawn({
cmd: [bunExe(), "test", "jsx.test.js"],
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
env: bunEnv,
});
const [, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

// Keep only the matcher output: from each `error:` line up to its stack trace.
const diffs = [...stderr.matchAll(/^error: expect\(received\)[^]*?(?=\n\s+at )/gm)].map(m => m[0]);
expect(diffs.join("\n")).toMatchInlineSnapshot(`
"error: expect(received).toEqual(expected)

Expected: <div id="expected" />
Received: <div id="received" />
error: expect(received).toStrictEqual(expected)

<ul>
- <li>two</li>
+ <li>one</li>
</ul>

- Expected - 1
+ Received + 1
error: expect(received).not.toEqual(expected)

Expected: not <div id="x" />"
`);
expect(exitCode).toBe(1);
},
);

it("test --preload supports global lifecycle hooks", () => {
const preloadedPath = join(tmp, "test-fixture-preload-global-lifecycle-hook-preloaded.js");
const path = join(tmp, "test-fixture-preload-global-lifecycle-hook-test.test.js");
Expand Down
Loading