RSC Test Helper
rsc-test-helper is a utility designed to enable unit testing of React components that consume async/await React Server Components (RSCs) within client-side testing environments like `@testing-library/react` or `react-test-renderer`. As of version 0.1.4, this package provides a `patch` function that transforms an async React component tree into a synchronous one by awaiting promises, thus making them compatible with standard React test renderers which do not natively support async component types. This is particularly useful for projects utilizing Next.js App Directory beta features (introduced in Next.js 13 in October 2022 and stabilized in 13.4 in June 2023), where RSCs return promises, causing errors in testing setups. The package currently has an early-stage development status with an undefined release cadence, focusing on solving an immediate testing pain point before official React/Next.js testing support for RSCs. A key differentiator is its specific focus on resolving the "Objects are not valid as a React child (found: [object Promise])" error encountered when rendering async RSCs in tests, especially in environments like JSDOM.
Common errors
-
Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
cause React test renderers (e.g., `@testing-library/react` or `react-test-renderer`) do not natively expect async function components (like React Server Components) to return a Promise directly. When an async component is included in JSX, its return value is a Promise, which React's reconciliation process cannot render as a valid child.fixApply the `patch` function from `rsc-test-helper` to your root component before rendering. For example: `const Component = await patch(<YourRootComponent />); render(<Component />);`. This transforms the async component tree into a synchronous one by resolving all promises.
Warnings
- gotcha The `patch` function, by its nature of awaiting all promises, prevents the direct testing of `<Suspense>` fallback states for the patched async components. It resolves the full component tree, bypassing intermediate loading states.
- gotcha This package is explicitly described as an 'early-stage development' and 'super dirty helper' (v0.1.4), indicating potential instability, unhandled edge cases, and that future updates, especially leading to a 1.0 release, may introduce breaking changes or significant refactors.
- gotcha The helper processes React elements in a client-side (e.g., JSDOM) testing environment. Server-side-only code, such as direct database access or other Node.js-specific APIs used within actual React Server Components, will not function correctly and may throw errors. The helper focuses on the React element transformation, not full server environment simulation.
Install
-
npm install rsc-test-helper -
yarn add rsc-test-helper -
pnpm add rsc-test-helper
Imports
- patch
const { patch } = require('rsc-test-helper');import { patch } from 'rsc-test-helper';
Quickstart
import React from "react";
import { render, screen } from "@testing-library/react";
import { patch } from "rsc-test-helper";
// Mocking internal components for a self-contained, runnable example.
// In a real application, these would be imported from their respective files.
const RoomChatParticipants = async () => {
// Simulate an async operation, like data fetching in a Server Component
await new Promise(resolve => setTimeout(resolve, 100));
return <div data-testid="chat-participants">Participants List (from RSC)</div>;
};
const RoomChatBox = ({ subheading }: { subheading: string }) => (
<div data-testid="chat-box">Chat Box: {subheading}</div>
);
const Skeleton = () => <div data-testid="skeleton">Loading participants...</div>;
// This component uses an async RSC (RoomChatParticipants)
const RoomPage = () => {
return (
<div>
<section>
<RoomChatBox subheading={"Welcome"} />
<React.Suspense fallback={<Skeleton />}>
{/* @ts-expect-error Server Component: This is a known Next.js/React pattern when using async RSCs directly in JSX */}
<RoomChatParticipants />
</React.Suspense>
</section>
</div>
);
};
describe("Room Page with Async RSCs", () => {
it("renders chat box and participants after patching async components", async () => {
// Without 'patch', render(<RoomPage />) would throw an error
// 'Objects are not valid as a React child (found: [object Promise])'.
// 'patch' transforms the async component tree to be synchronously renderable.
const ComponentToRender = await patch(<RoomPage />);
render(<ComponentToRender />);
// Verify synchronous client components are rendered
const chatBox = screen.getByTestId("chat-box");
expect(chatBox).toBeInTheDocument();
expect(chatBox).toHaveTextContent("Welcome");
// Verify the async RSC content is rendered after patching
const participants = screen.getByTestId("chat-participants");
expect(participants).toBeInTheDocument();
expect(participants).toHaveTextContent("Participants List (from RSC)");
// The skeleton fallback for Suspense is bypassed by 'patch' because promises are awaited.
expect(screen.queryByTestId("skeleton")).not.toBeInTheDocument();
});
});