Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
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 @@
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`.

Check warning on line 497 in src/runtime/test_runner/pretty_format.rs

View check run for this annotation

Claude / Claude Code Review

Multi-line comment violates REVIEW.md one-line rule

REVIEW.md's comment rule is "One line. … Prefer links to GitHub issues" — the original comment here was one line, this one spans three and narrates the React 19 rename (the comment-cop bot has already flagged it). The sibling check in `ConsoleObject.rs:2254` uses the local convention: `// For React 19 - https://github.com/oven-sh/bun/issues/17223`. Suggest keeping `// Is this a react element?` and putting the #17223 link on the `react.transitional.element` entry instead of the prose.
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
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