Files
WRNexusJS/packages/react/test/error-boundary.test.tsx
T
ClintchizandClaude Opus 5 df2ee036eb feat(react): add per-island error boundary
A crashed island renders its error in dev and nothing in prod, leaving
the surrounding server-rendered page intact.

Island .tsx sources carry an explicit @jsxImportSource react pragma: the
repo's root tsconfig points jsxImportSource at @wrnexus/core, so without
it island JSX compiles to WRNexus's string renderer instead of React
elements.

Tests render on the client via createRoot rather than a server renderer,
because React error boundaries do not engage during SSR — and islands
are client-only regardless.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:16:42 +05:30

105 lines
2.8 KiB
TypeScript

/** @jsxImportSource react */
import { afterEach, expect, test } from "bun:test";
import { Window } from "happy-dom";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { IslandErrorBoundary } from "../src/error-boundary.tsx";
// React error boundaries only engage during client rendering — the server
// renderers rethrow. Islands are client-only, so this is also how they run.
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function Boom(): never {
throw new Error("chart exploded");
}
function mountHost() {
const window = new Window();
(globalThis as any).window = window;
(globalThis as any).document = window.document;
const container = window.document.createElement("div");
window.document.body.appendChild(container);
return { window, container: container as unknown as HTMLElement };
}
const silenced: Array<() => void> = [];
afterEach(() => {
for (const restore of silenced.splice(0)) restore();
});
function silenceExpectedErrors() {
const original = console.error;
console.error = () => {};
silenced.push(() => {
console.error = original;
});
}
test("renders children when nothing throws", async () => {
const { container } = mountHost();
const root = createRoot(container);
await act(async () => {
root.render(
<IslandErrorBoundary name="Chart" development={false}>
<p>ok</p>
</IslandErrorBoundary>,
);
});
expect(container.innerHTML).toBe("<p>ok</p>");
});
test("contains a thrown error and shows details in development", async () => {
silenceExpectedErrors();
const { container } = mountHost();
const root = createRoot(container);
await act(async () => {
root.render(
<IslandErrorBoundary name="Chart" development>
<Boom />
</IslandErrorBoundary>,
);
});
expect(container.innerHTML).toContain("Chart");
expect(container.innerHTML).toContain("chart exploded");
});
test("renders nothing in production when an island throws", async () => {
silenceExpectedErrors();
const { container } = mountHost();
const root = createRoot(container);
await act(async () => {
root.render(
<IslandErrorBoundary name="Chart" development={false}>
<Boom />
</IslandErrorBoundary>,
);
});
expect(container.innerHTML).toBe("");
});
test("a crashed island does not remove sibling server markup", async () => {
silenceExpectedErrors();
const { window, container } = mountHost();
const sibling = window.document.createElement("p");
sibling.textContent = "server rendered";
window.document.body.appendChild(sibling);
const root = createRoot(container);
await act(async () => {
root.render(
<IslandErrorBoundary name="Chart" development={false}>
<Boom />
</IslandErrorBoundary>,
);
});
expect(window.document.body.textContent).toContain("server rendered");
});