Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 12 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,20 @@ impl Tag {
return Tag::get(value.get_proxy_target(), global_this);
}

// Is this a react element?
// Is this a react element? Same set of `$$typeof` symbols as the console
// formatter (`ConsoleObject.rs`): React 19 renamed `react.element` to
// `react.transitional.element`.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
Outdated
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", 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
47 changes: 47 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,53 @@ 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, like Bun.inspect
// prints them, 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", () => {
expect(h("ul", { className: "list", children: h("li", { children: "one" }) }))
.toMatchInlineSnapshot(`<ul className="list">
<li>one</li>
</ul>`);
expect(h("ul", { children: [h("li", { children: "one" }, "1"), h("li", { children: "two" }, "2")] }))
.toMatchInlineSnapshot(`<ul>
<li key="1">one</li>
<li key="2">two</li>
</ul>`);
});

test("elements inside other values serialize as JSX", () => {
expect({ el: h("div", { id: "x" }) }).toMatchInlineSnapshot(`
{
"el": <div id="x" />,
}
`);
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
// Bun.inspect does, 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