RSC Test Helper

0.1.4 · active · verified Tue Apr 21

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

Warnings

Install

Imports

Quickstart

This quickstart demonstrates how to use `rsc-test-helper`'s `patch` function to enable unit testing of a React component tree that includes an asynchronous React Server Component (RSC) within a standard testing environment like `@testing-library/react`. It shows how to resolve the common 'Objects are not valid as a React child' error by awaiting and transforming the component before rendering.

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();
  });
});

view raw JSON →