Testing Nested Server Components
Asked Answered
C

3

7

I've successfully tested one isolated server component by using an async it and awaiting the output of a call to the component as a plain function to pass into render:

  it('renders the user data when user is authenticated', async () => {

    render(await UserData())

    expect(screen.getByTestId('user-data')).toBeInTheDocument();
  });

^ That works and passes.

But I also have a page.tsx server component that renders another server component -- <UserData /> -- nested within it:

'use server'

import styles from './page.module.css';
import LogInOut from "./LogInOut/LogInOut";
import UserData from "./UserData";

export default async function Page() {
  return (
    <main className={styles.main}>
      <h1>Title</h1>
      <LogInOut/>
      <UserData />
    </main>
  );
}

// Note: LogInOut is a client component, removing it doesn't help the problem.

... and if I try the same technique to test this component, i.e.:

  it('renders a heading', async () => {

    render(await Page());

    const heading = screen.getByRole('heading', { level: 1 });

    expect(heading).toBeInTheDocument();
  });

^ ...this does not work -- I get an error that reads:

Error: Uncaught [Error: Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.]

How can we test nested server components? I want a solution that allows me to render both the parent and descendent server component in a unit test, without mocking one or the other.

Chambertin answered 10/2, 2024 at 1:15 Comment(3)
See also testing-library/react-testing-library issue 1209 and, for illustration, vitejs/vite-plugin-react issue 325.Chez
See also the latest issue 1209 commentChez
I found this custom render function on issue 1209 which works perfectly well for me.Tangle
S
1

It seems like there's not much support for react-testing-library, see here. What about using Suspense, have you tried that?

import { render, screen } from "@testing-library/react";
import { Suspense } from "react";

it('renders a heading', async () => {
  render(
    <Suspense>
      <Page />
    </Suspense>
  );

  const heading = await screen.getByRole('heading', { level: 1 });

  expect(heading).toBeInTheDocument();
});
Spam answered 15/2, 2024 at 15:58 Comment(0)
T
1

The discussion is still ongoing on Issue 1209 of React Testing Library :

  • For nested async components, you will indeed face an error of trying to render a promise, because the current workaround is to render Server Components in the client, which do not support async Components.
    This custom render function hacks React internals to render server components in tests. It works by taking async children one by one, and prerendering them manually.

  • For simpler cases (no nesting), you should simply using Suspense.

Tangle answered 8/10, 2024 at 11:3 Comment(0)
Y
-1

The error you're encountering is because you are trying to render a React component asynchronously using await, but the render function expects a synchronous component.

try this way:

it('renders a heading', async () => {
  const page = await Page(); // Resolve the promise before rendering
  render(page);

  const heading = screen.getByRole('heading', { level: 1 });

  expect(heading).toBeInTheDocument();
});
Yonit answered 16/2, 2024 at 13:4 Comment(5)
Inlining the await Page() or saving it in a variable makes no difference whatsoever. render is still receiving the resolved component regardless.Zoroastrianism
should make a big difference. By awaiting the "page" function outside of the "render" function, you ensure that the page variable contains the resolved component before passing it to render. This should resolve the error you're encountering.Yonit
Nope, the render function already receives the component because you're using await. It'd be a different story if you tried to do render(Page()), in that case it wouldn't work because it's receiving a Promise<Component>. When awaiting a promise, it gets unwrapped into its resolved type, so using await Page() would yield a type Awaited<Promise<Component>> which is just Component. Whether you save it as a separate variable or inline it it makes no difference since render won't execute until the Page() promise is resolved.Zoroastrianism
have you at least tried?Yonit
I have exactly the same problem and confirm it does not make any difference.Tangle

© 2022 - 2025 — McMap. All rights reserved.