Support for React Server Components
remy90 opened this issue Β· comments
Describe the feature you'd like:
As a user of react 18 with NextJS (with app directory), I would like to render async server components
example:
// Page.tsx
const Page = async ({ params, searchParams }: PageProps) => {
const asyncData = await fetchSomeDataAsynchronously()
return (<foo {...asyncData} />
}
...
// Page.test.tsx
it('should render', () => {
render(<Page />)
expect(screen....).ToBe(....)
})
Extracting server page logic would be an alternative, but I think that would also significantly reduce the purpose of RTL if that were to become an encouraged architectural default.
Current progress, workarounds, and demo
You can use like this:
it('should render', async() => {
render(<Page />);
await waitFor(()=> {
expect(screen....).ToBe(....)
});
});
@prakashtiwaari Not quite.
It's render
that's giving the issue. To provide the full error message:
'Page' cannot be used as a JSX component.
Its return type 'Promise<Element>' is not a valid JSX element.
Type 'Promise<Element>' is missing the following properties from type 'ReactElement<any, any>': type, props, keyts(2786)
A friend came up with the following:
const props = {
params: { surveyType: '123' },
searchParams: { journeyId: '456' },
}
const Result = await Page(props)
render(Result)
I'll wait for a moderator to close this but it would be nice to have async components supported inside render
Once TypeScript 5.1 is out (scheduled for 30th of May) we'll land DefinitelyTyped/DefinitelyTyped#65135 which fixes this issue.
A friend came up with the following:
const props = { params: { surveyType: '123' }, searchParams: { journeyId: '456' }, } const Result = await Page(props) render(Result)I'll wait for a moderator to close this but it would be nice to have async components supported inside render
I like this idea personally. It should be easy enough to introspect if a component is async by determining if it returns a Promise. Then we can await the Promise internally and either return a Promise in render or poll to return synchronously.
For those that are trying to mock API responses, this seems to be working for now:
test("should do stuff", async () => {
global.fetch = jest.fn().mockResolvedValue({
json: jest.fn().mockResolvedValue({ login: "Gio" }),
});
render(await Page());
expect(screen.getByText("Hi Gio!")).toBeInTheDocument();
});
If you don't define global.fetch
, you get ReferenceError: fetch is not defined
.
I would love to be able to use MSW, but that's still not working with Next's model. See: https://twitter.com/ApiMocking/status/1656249306628915208?s=20
On a side note, even if we make this work, there's the issue of Next layout composition. If I render a Page
component in my test, I most likely want to render also its layout(s). I understand this is not necessarily a concern that RTL should have, but we should keep it in mind. Next could provide an API for testing that builds the whole rendering tree.
We could document using layouts similarly to how we already document using providers: by importing them and passing them as the wrapper
option of render
.
import {render} from '@testing-library/react'
import Layout from './layout'
import Page from './page'
render(<Page />, {wrapper: Layout})
@Gpx has a good point that rendering nested layouts would be more challenging. We could try to expose an abstraction, but the paths to import from would be dynamic and depend on Next patterns. If this isn't practical, we can open an issue with Next upstream and ask for an API to consume internally.
Also if we change render to be compatible with returning promises, we should probably ship a breaking change so end users are aware they may need to change their tests (especially if they're using TypeScript).
I agree we should ask Next to provide an API. I'm thinking something like this:
const RSC = routeComponent('/some/path') // This will return an RSC with all nested layouts passing the correct props
render(RSC)
// With additional props
const RSC = routeComponent('/some/path', { foo: 1 })
render(RSC)
WDYT?
As for the breaking change, won't this be a feature? I don't think anyone was relying on render
breaking for async components.
I'm doing more research into RSCs using Next, and I noticed some mistaken assumptions I had:
- Determining if a component uses suspense is not possible due to the halting problem as it would be valid for the component to sometimes not return a Promise, and we can't distinguish between a component not returning a Promise currently vs. ever.
- The new rendering async APIs don't return Promises, they return streams. So, we don't necessarily need to make our
render
function async, though we may need to poll to unwrap streams synchronously.
Overall, I think supporting React server components involves figuring out the following concerns:
- DefinitelyTyped/DefinitelyTyped#65135
- Figuring out how we can internally render async components (may involve using a different render API, and would require using experimental/canary React right now)
- Supporting bundling and routing strategies from different frameworks (we could use the adapter pattern to compose APIs from RSC frameworks such as vercel/next.js#50479)
I agree we should ask Next to provide an API. I'm thinking something like this:
const RSC = routeComponent('/some/path') // This will return an RSC with all nested layouts passing the correct props render(RSC) // With additional props const RSC = routeComponent('/some/path', { foo: 1 }) render(RSC)WDYT?
@Gpx Shouldn't that be render(<RSC />)
? Also are you suggesting the URL path or filesystem path here?
The idea to render the server component as async client components seems to be the best idea I've seen so far.
I don't think it's suitable for RTL to bake Next.js' (or any other framework's) semantics into it's API. Async client components solve this by being agnostic of any framework.
I think this would retain the current RTL API with maybe the addition of a suspense wrapper - rendering nothing initially could be confusing so enforcing a fallback makes sense.
render(<Page />) // throws an error because there's no boundary (I think this would be default react behaviour)
render(<Suspense><Page /></Suspense>) // This would be ok
For anyone that wants to help, I'm trying to figure out why I can't hydrate into a server rendered document, it's like it's destroying the whole document. https://gist.github.com/nickmccurdy/a5797bec9bb7e1156f814846c9bcb04b https://github.com/nickmccurdy/rsc-testing
Why are you trying to server render? RSC doesn't rely on SSR, that's an optional optimisation step. See https://github.com/dai-shi/wakuwork for an example of this
You need to call createFromFetch/readable from the react-server-dom-webpack/client and pass that element to react Dom to render. This ofc relies on having access to a RSC stream which you can create from the same package.
I'd rather not add APIs specific to bundlers, especially since Next has already moved to other bundlers and we still have others to support in the future.
That isn't really a bundler specific API, it includes the shared pieces shared across all bundlers. Source.
I do think it's easier to just mount the server component as a client component with createRoot though - this works in react-dom canary today.
See commit on your test repo here: tom-sherman/rsc-testing@9e4aa67
I'm not sure how RTL can support rendering of the root layout in Next.js, but as I said before I don't think it should - at least not part of it's core API. The root layout returning <html>
is a Next.js-specific idiom, not all server component frameworks choose to work this way.
Thanks for the branch, that's interesting. I'd still like to understand why this works and onAllReady
doesn't though, as that's supposed to be used for SSG where having suspense fallbacks everywhere wouldn't be accessible. Also, while I understand why your version of the test needs to await for a suspended render, I'd rather not make end users worry about understanding this if there's a way to ensure certain components load automatically. Additionally, both branches still have act
warnings, though I have no idea how to fix this as I've already tried using it in various ways.
Also, while I understand why your version of the test needs to await for a suspended render, I'd rather not make end users worry about understanding this if there's a way to ensure certain components load automatically
You're going to need an await
somewhere - you can't syncronously block because it's an async component. I suppose you could have a builtin suspense boundary and have the user await render(<Page />)
but as soon as the user adds a suspense boundary somewhere they're gonna need to use waitFor anyway.
That's closer to what I was thinking originally. Though, I think I'm confused about how this is working, but I'll reply here again when I resolve it.
I agree we should ask Next to provide an API. I'm thinking something like this:
const RSC = routeComponent('/some/path') // This will return an RSC with all nested layouts passing the correct props render(RSC) // With additional props const RSC = routeComponent('/some/path', { foo: 1 }) render(RSC)WDYT?
@Gpx Shouldn't that be
render(<RSC />)
? Also are you suggesting the URL path or filesystem path here?
No, I think it should be render(RSC)
where RSC is something like <Component {...nextProps} >
. Alternatively routeComponent
could return a component and its props:
const { Component, props } = routeComponent('/some/path')
render(<Component {...props} />)
We need not only the components tree but also the props that Next is passing.
The URL should be passed to the method, not the filesystem path. If we want to simulate a real user interacting with the app they'll use URLs.
To be clear, I'm not saying we should implement this in RTL, but rather that we should ask the Next team to provide it since it will be helpful for anyone doing tests.
I'm not sure we need it to return params, since you can just choose what params to render in your test by changing the URL.
I'm not sure we need it to return params, since you can just choose what params to render in your test by changing the URL.
Say you want to test the URL /foo/bar/baz
. What are the params
? Well, it depends on your file system:
Route | params |
---|---|
app/foo/[slug]/baz |
{ slug: 'bar' } |
app/foo/bar/[slug] |
{ slug: 'baz' } |
app/foo/[[...slug]] |
{ slug: ['bar', 'baz'] } |
I can create the params
object in my tests and pass it to the component, but if later I modify the filesystem, I might break my code, and my tests will still work.
If we want to keep the render(<Component />)
format rather than render(Component)
Component
could just render the tree passing the correct params
.
Do we agree that RTL (at least in the core API) shouldn't support this kind of routing stuff? If so probably best to split that conversation out into a different issue?
I agree, I'll open an issue in Next's repo
If we want to keep the
render(<Component />)
format rather thanrender(Component)
Component
could just render the tree passing the correctparams
.
@Gpx Yes, I think that's simpler, and I'd rather avoid exposing the implementation detail of param values.
Do we agree that RTL (at least in the core API) shouldn't support this kind of routing stuff? If so probably best to split that conversation out into a different issue?
@tom-sherman I'm not suggesting we add a Next specific app router API directly into Testing Library. However, I'd like us to have either docs or some sort of adapter/facade/etc. design pattern that composes a Next routing primitive.
Guys any example how i can test pages inside app/[locale] folder with providers (next-intl)?
Because i am getting
invariant expected app router to be mounted
This is my test file
import { render, screen } from '@testing-library/react';
import Home from '@/app/[locale]/page';
import Layout from '@/app/layout';
import Header from '@/components/header';
import { Providers } from '@/app/providers';
import Footer from '@/components/footer';
import SessionModal from '@/components/Modals/SessionModal';
import { NextIntlClientProvider } from 'next-intl';
import messages from '../messages/en.json';
describe('Home', () => {
it('renders a heading', () => {
render(
<NextIntlClientProvider locale={'en'} messages={messages}>
<Header />
<Providers>
<Home />
</Providers>
<Footer />
<SessionModal />
</NextIntlClientProvider>,
{ wrapper: Layout }
);
expect(screen.getByRole('heading')).toHaveTextContent('Letβs Get Started!');
});
});
@DonikaV Could you share a full repository or Gist that reproduces the error?
@nickmccurdy hey, no i can't but i resolved finally it, errors was because of useRouter()
This helps me
jest.mock('next/navigation', () => ({
...require('next-router-mock'),
useSearchParams: () => jest.fn(),
}));
FYI request for a testing method in Next vercel/next.js#50479
Progress
I had some helpful suggestions from the React team on how to start up an RSC server with full support for React RSC features (excluding Next specific features for now). I'm developing a renderServer
function that simulates a React server with our existing React client.
Full integration with Next's app router depends on vercel/next.js#50479.
Workarounds
You can test most async components with React 18.3 (canary) or 19 (rc):
import { render, screen } from "@testing-library/react";
import { Suspense } from "react";
import Page from "./page";
test("Page", async () => {
render(
<Suspense>
<Page />
</Suspense>,
);
await screen.findBy...(...); // first assertion must await for suspense
// additional assertions may follow
});
You may want to use a custom render
function to simplify test setup if your suite heavily relies on async components.
If you need other RSC (i.e. server actions) or app router (i.e. layouts) features you can use hard coding, mocks, or an e2e test framework until we figure out these issues.
server-only
errors
Some React Server Components import the server-only
module to prevent accidental usage in Client Components, resulting in this error:
This module cannot be imported from a Client Component module. It should only be used from a Server Component.
React Testing Library doesn't have a server yet, so it needs to render Server Components in its client for now.
If you're using Jest or Vitest, you can disable the module's error with an empty mock script named __mocks__/server-only
.
With Vitest, you'll also need to manually register it:
vi.mock("server-only");
Alternatively you can mock the module in a setup or test file, for example:
jest.mock("server-only");
vi.mock("server-only", () => ({}));
TypeScript errors
Use typescript@^5.1.2
and @types/react@^18.2.8
to fix this error when rendering async components:
'...' cannot be used as a JSX component. Its return type 'Promise' is not a valid JSX element. Type 'Promise' is missing the following properties from type 'ReactElement<any, any>': type, props, key
React warnings
Newer versions of React 18.3 (canary) and 19 (rc) added warnings when rendering server components in clients:
Warning: async/await is not yet supported in Client Components, only Server Components
Warning: A component was suspended by an uncached promise. Creating promises inside a Client Component or hook is not yet supported, except via a Suspense-compatible library or framework.
However, tests following my suggestions should still work for now. I believe the warning mainly exists to prevent accidental usage of async components without a meta framework and inform users about potential instability. Remember to pin your React version in a shared package.json
or lockfile though, as changes in canaries and rcs could be breaking.
Demo
Commenting to follow this - I'm also interested in this topic. I spent some time thinking about a similar problem, "how to test Next.js SSR behavior in Cypress", of which some thoughts are documented here. I don't have a great solution yet, but am very interested in a general solution for testing components that require server behavior, which is increasingly common with SSR frameworks and innovations like RSC.
Mock/stubbing things can work for some use cases, but if the the paradigm of moving more and more things to the server side continues, I think we will need something a bit more robust and production like to write reliable tests.
I personally think Cypress is the best way to test server components in an application (Next.js or any other future RSC framework). My opinion is mostly informed by the thread and Dan's tweet here.
Cypress (with optionally mocked network calls to APIs) is the best choice because it does a proper integration test of the entire lifecycle of a component inside of your framework. So much of RSCs are implemented in the framework and router layer that you're often not testing them very well with unit tests in a fake environment.
I think though that these unit tests are valuable for library maintainers - it's not feasible for them to run integration tests for every framework that exists. Instead they can target their testing with the parts of RSC that are implemented in React. This is where RTL comes in I think, it can help to provide a nice API and DX for those component library developers.
Server rendering async components is not different to server rendering any other components. The APIs are not different (otherwise it wouldn't compose). Though I see how using renderToPipeableStream
looks unwieldy so I'll definitely try to come up with a light wrapper around it. The hard part here will be deciding on how granular the streaming aspect should be tested. Probably best to start with an approach the just waits for everything to load and then return the final result.
The initial issue description poses a much harder problem: How do we entangle the mix of environments?
render(<Page />)
//^^^^^^^^^^^^^^^^ should probably run on the server
expect(screen....).ToBe(....)
//^^^^^^^^^^^^^^^^ is this supposed to use the DOM i.e. run in a browser?
Mixing these two has a lot of pitfalls that needs carefully crafted tests and environments. We're already facing issues with this in the React codebase and don't have a good solution for it yet.
Or is the issue about testing RSC (e.g. ensuring server code like server actions only runs on the server, client code only on the client) which needs a framework. It's probably better to defer that to the frameworks itself since they also take care of e.g. ReactDOM.preload()
, ReactDOM.preinit()
or server actions. At this point you're better off using the e2e testing framework of your choice (e.g. Playwright or Cypress) because that ensures you don't accidentally test implementation details of the framework.
I personally think Cypress is the best way to test server components in an application
But what if your application is made of RSC? Won't use Cypress or other E2E lead to slow tests?
Cypress (with optionally mocked network calls to APIs) is the best choice because it does a proper integration test of the entire lifecycle of a component inside of your framework.
You can mock browser network calls but can't mock calls made by the server component. In other words, with a component like this, I can't mock the fetch
call:
export default async function Page() {
const user = await fetch('/user/1');
return <div>{user.name}</div>
}
I don't have a solution to these problems, but as of now, using E2E test is not a suitable solution for most applications.
You can use a proxy (or a stubbed fetch or API client) to accomplish the mocked network calls, hopefully Mock Service Worker will support server components in Next.js eventually to make this easier.
I agree it's not entirely practical to write an End to End test for every permutation of each RSC. That said, given:
export default async function Page() {
const user = await fetch('/user/1');
return <div>{user.name}</div>
}
If you mock fetch, what are you really testing here? Considering these components are designed to render on the server, I think we need to build some tooling that uses the server, at least in some capacity. If you stub out all the server calls, it's just a client component, at that point.
I think that RSC really do should be integrated tested, at least to to an extend, depending what your component does. If it needs to use fs
to read something, maybe using a real fs
is better - it's definitely more production like. I haven't worked with RSC in prod enough to get an idea of what kind of components people would like to test, is there any good real world resources?
Firstly, you'd want to test what happens when that fetch failed.
In the real world RSC do a lot of things at runtime:
- Load client components
- Render client components and pass props down
- Load resources such as styles
- Streamed response with out of order streaming
All of these things can happen dynamically or based on some logic that you would want to test.
A lot of that is implemented inside the framework which is why an integration/e2e test is needed
I agree it's not entirely practical to write an End to End test for every permutation of each RSC
I'm not suggesting this btw, at least not for application developers. I'm suggesting performing high-fidelity tests against the framework using Cypress or similar (this is what's recommended by the React team). You would test routes not components. This makes sense to me because RSC are as much a routing solution than anything else.
I'm still researching this and interested in finding a common ground of testing APIs that would be useful for integration testing any RSC framework. Now that there are multiple attempts at adding different RSC renderers to React core, I'll try to figure out testing adapters that could work without being too tied to framework implementation details.
To some extent you can just use async functions in the client which is probably the easiest way to test it.
Ultimately, the thing blocking us from adding something really good that Just Works correctly in a unit testing running like Jest is the ability to run two different environments for one test. We have a workaround in the React repo for our own tests but it's not great we're pretty close to just ripping out Jest for our tests for this reason.
Jest has a way to test Node.js code in isolation with @jest-environment node
which works fine for testing SSR output or RSC output. Jest also has a way to test client side like @jest-environment jsdom
which works fine to test client-only rendering of components.
The problem is that there's no way for the same test two spawn both - and get the right export conditions set up in the module resolution. You can hack around it in various ways by setting up an environment that tries to be both but it won't test exactly the right thing and a key feature of RSC is that "server-only" and "client-only" can be mutually exclusive environments and we can have the same code run differently in each one.
This is not really a new thing, this has been an issue with isomorphic code like getServerSideProps / loaders and client code too. Bugs has slipped past like trying to access a database client-side which worked in the tests. It's just that we're trying to define and take advantage of these boundaries deeper now.
To do this properly we need test runners (vitest too ideally) to adopt a way to run two environments for a single test so you can run RSC, SSR, hydrate it and assert on the result.
It doesn't have to be a full E2E integration but having the ability to run two environments would allow us to build official lightweight bindings for rendering React in that environment. Right now it's a bit difficult to do that which isn't super hacky.
It sounds like we need to fix or replace Jest before we can land this in RTL π
Well not necessarily. Depends on what strategy you want. E.g. you can support async function in client components as an approximation. There can also be a different mode that only executes the Server Components and not the client component shallowly. It's just that the ideal solution would ideally use two graphs.
Alternatively, couldn't the server renderer be spawned in a different environment by Testing Library (potentially using RPC) so we can keep the test assertions running in a client environment? I don't think it really makes sense to test a server's rendering without a client, considering there doesn't seem to be any meta framework that actually supports this yet, Testing Library's APIs are designed to work in browsers, and we want to test user interactions rather than internal framework implementation details.
Yea, if it can spawn a new process and it's enough to reuse the built-in Node.js module resolution it could be doable. Might miss out on some jest features like file watching, mocking etc that way but maybe it's enough.
It is a real shame that it appears testing is an afterthought when it comes to server components :(
The general strategy we recommend for React is E2E with Playwright or Cypress for a number of reasons. So thatβs the thought. I know not everyone agrees with that testing strategy and want to try other thing and youβre free to.
Thanks @sebmarkbage . That is a strategy I've been considering myself recently. Are you doing component testing this way as well?
Do you have a mocking strategy at all that would help you test out different scenarios based on different data returned in component tests in this way?
Sorry I said a mocking strategy based on component tests but I meant for testing server components in general including how you'd test pages?
@citypaul Let's keep this discussion focused on React Testing Library please, there are better places to discuss React Server Components and testing in general. If you want to mock a network resource, we'd recommend using Mock Service Worker (example).
@citypaul Let's keep this discussion focused on React Testing Library please, there are better places to discuss React Server Components and testing in general. If you want to mock a network resource, we'd recommend using Mock Service Worker (example).
Sure, apologies for de-railing! :)
The moment I use async
on the server component and I do an await
inside the RSC I get
Jest encountered an unexpected token
Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
β’ If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
β’ If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
β’ To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
β’ If you need a custom transformation specify a "transform" option in your config.
β’ If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
[/Users/user/Apps/rap-portal-fe/node_modules/.pnpm/jose@4.14.4/node_modules/jose/dist/browser/index.js:1](mailto:/Users/user/Apps/rap-portal-fe/node_modules/.pnpm/jose@4.14.4/node_modules/jose/dist/browser/index.js:1)
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { compactDecrypt } from './jwe/compact/decrypt.js';
^^^^^^
SyntaxError: Unexpected token 'export'
11 | <div className='nhsuk-width-container'>
12 | <h1>Hello</h1>
> 13 | </div>
| ^
14 | )
15 | }
16 |
at Runtime.createScriptFromCode ([../../node_modules/.pnpm/jest-runtime@29.5.0/node_modules/jest-runtime/build/index.js:1495:14](mailto:../../node_modules/.pnpm/jest-runtime@29.5.0/node_modules/jest-runtime/build/index.js:1495:14))
at Object.<anonymous> ([../../node_modules/.pnpm/openid-client@5.4.2/node_modules/openid-client/lib/client.js:8:14](mailto:../../node_modules/.pnpm/openid-client@5.4.2/node_modules/openid-client/lib/client.js:8:14))
at Object.<anonymous> ([../../node_modules/.pnpm/openid-client@5.4.2/node_modules/openid-client/lib/issuer.js:5:19](mailto:../../node_modules/.pnpm/openid-client@5.4.2/node_modules/openid-client/lib/issuer.js:5:19))
at Object.<anonymous> ([../../node_modules/.pnpm/openid-client@5.4.2/node_modules/openid-client/lib/index.js:1:36](mailto:../../node_modules/.pnpm/openid-client@5.4.2/node_modules/openid-client/lib/index.js:1:36))
at Object.<anonymous> ([../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/lib/oauth/callback.js:8:21](mailto:../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/lib/oauth/callback.js:8:21))
at Object.<anonymous> ([../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/routes/callback.js:10:40](mailto:../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/routes/callback.js:10:40))
at Object.<anonymous> ([../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/routes/index.js:39:40](mailto:../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/routes/index.js:39:40))
at Object.<anonymous> ([../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/index.js:14:38](mailto:../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/core/index.js:14:38))
at Object.<anonymous> ([../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/next/index.js:10:13](mailto:../../node_modules/.pnpm/next-auth@4.22.1_next@13.4.3_react-dom@18.2.0_react@18.2.0/node_modules/next-auth/next/index.js:10:13))
at Object.<anonymous> (src/app/page.tsx:13:15)
at Object.<anonymous> (__tests__/pages/Home.test.tsx:16:54)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 0.492 s, estimated 1 s
Ran all test suites related to changed files.
I've been troubleshooting this the whole day
@netdesignr Jest doesn't require special config for async React Server Components, though you'll likely want to use async/await and JSX. I've pushed a jest
branch to my rsc-testing
repository as an example. If that doesn't solve your issue, please post a reproduction to our Discord.
@nickmccurdy thanks for the reply. Question, did you get a chance to add an await inside Page
RSC? I will be trying and replicating based on your settings but I thought I should also ask.
@nickmccurdy thanks for the reply. Question, did you get a chance to add an await inside
Page
RSC? I will be trying and replicating based on your settings but I thought I should also ask.
That should work the same. The Page
component in my demo is already async
.
@nickmccurdy thanks for the reply. Question, did you get a chance to add an await inside
Page
RSC? I will be trying and replicating based on your settings but I thought I should also ask.That should work the same. The
Page
component in my demo is alreadyasync
.
Although in my code base I will be needing to have either await for the userSession or for some backend API calls I was trying to use your code and try things around.
import Client from './client'
export default async function Home() {
const a = await fetch(
'https://my-json-server.typicode.com/typicode/demo/posts',
)
return (
<>
<p>server</p>
<Client />
</>
)
}
Have you got any basic examples on mocking fetch inside jest or even better having basic nextauth and check against those basic stuff? If you're too busy I could try myself and send a PR rather than forking your repo?
Btw, I still see some Vitest traces, I wondered if they needed gone too?
I know this is just a demo repo, but maybe we can add more stuff on it as I can see other people going around for these things and there's no support/docs for it.
Although in my code base I will be needing to have either await for the userSession or for some backend API calls I was trying to use your code and try things around... Have you got any basic examples on mocking fetch inside jest or even better having basic nextauth and check against those basic stuff?
If you use the Suspense
workaround, mocking with msw should work normally.
Btw, I still see some Vitest traces, I wondered if they needed gone too?
Good point, I've deleted the tsconfig.node.json
and some dependencies we're no longer using.
I know this is just a demo repo, but maybe we can add more stuff on it as I can see other people going around for these things and there's no support/docs for it.
I don't intend on making rsc-testing a kitchen sink example, but if you have other suggestions related to RSCs I'm open to issues and pull requests.
As a follow-up, I'm having the exact same issue as @netdesignr. Netdesignr, any luck solving this?
Unfortunately no news as I could not overcome the issues however I went. I tried @nickmccurdy example, I tried the things in the above comments no success. Spent three days battling this issue and now I look for approaching this by using the nx/next monorepo. I'm literally migrating an existing NextJS 12 stack to latest stack for a production application and when I thought everything was moving smooth got into this basic stuff that's required to be done in any application...
@netdesignr Based on the discussion so far, I'm not sure what the remaining issues are, and I'd like to keep it focused on React Server Components. If you need more help, please open a new issue or post on Discord with code reproducing the issue and steps you've tried to fix it.
Suggestion of a custom render function:
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { Suspense } from 'react';
export async function renderWithSuspense(...args: Parameters<typeof render>) {
const [ui, ...otherArgs] = args;
const result = render(<Suspense fallback="__LOADING__">{ui}</Suspense>, ...otherArgs);
screen.getByText('__LOADING__');
await waitForElementToBeRemoved(() => screen.queryByText('__LOADING__'));
return result;
}
Usage:
import { screen } from '@testing-library/react';
import { renderWithSuspense } from './renderWithSuspense';
async function MyComponent() {
const data = await getData();
return <p>{data}</p>;
}
test("render", async () => {
await renderWithSuspense(<MyComponent />);
screen.getByText('Hello, World!');
});
Hello, using renderWithSuspense
and mix of client component
and server component
we have a warning (it only appear in testing). Do you have an idea how to fix this warning
Warning: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server.
How to reproduce :
function ClientComponent({ children }: { children: React.ReactNode }) {
return <main>{children}</main>;
}
async function ServerComponent() {
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
return <section>test</section>;
}
it.only("should not render warning", async () => {
await renderWithSuspense(
<ClientComponent>
<ServerComponent />
</ClientComponent>
);
});
Apologies for the delay, I'm still catching up with updates to RSCs.
@tkrotoff I like this direction and almost thought of proposing something myself, but it may break if there's other Loading...
text or additional suspense boundaries are needed to load async components. Feel free to use something like this if you want, but I'd rather not publish or document an abstraction officially until we can either fix those issues or get a real React server working.
@JustinMartinDev React 18.3 canary recently added Warning: async/await is not yet supported in Client Components, only Server Components
when rendering async components. I believe it's there mainly to warn about accidental usage of async components without a meta framework and its potential instability, but our tests still pass with the development build of React. Remember to commit lockfiles for dev environments though, as changes in canaries could be breaking.
Hey guys, I came up with this solution:
const renderPage = async () => render(
await Page(),
{
wrapper: ({ children }: { children: ReactNode }) => (
<Providers>
<App>{children}</App>
</Providers>
)
}
);
it("Renders successfully", async () => {
const { container } = await renderPage();
expect(container).toBeTruthy();
});
Hope it helps!
Unfortunately this approach still breaks when child components are async.
Unfortunately this approach still breaks when child components are async.
Can you heck if your component is memoized or has some kind of wrapper?
Memoizing is a performance optimization, I wouldn't recommend relying on it in tests.
Linking a Stack Overflow solution that helped me
https://stackoverflow.com/a/76267736
Rendering the component as a function within an act
call worked
import { act } from "react-dom/test-utils";
await act(async () => {
render(
await Component({ children: <></>, // other props })
);
});
I went one step further and made a util to do this with generic components
import { render } from "@testing-library/react";
import { act } from "react-dom/test-utils";
export const renderServerComponent = (
Component: () => Promise<React.ReactElement> | React.ReactElement,
) =>
act(async () =>
render(
await Component()
)
);
In your tests, use it like this:
const Component = ({children}: {children: React.ReactNode;}) => <div>{children}</div>
const { container } = await renderServerComponent(() => Component({ children: <>Hello world</>, // other typed props}));
This has the same problem I mentioned above:
Unfortunately this approach still breaks when child components are async.
This has the same problem I mentioned above:
Unfortunately this approach still breaks when child components are async.
Why is it breaking, can you share your code?
In my case it's also failing because the page has nested async components that utilize suspense and loading state in children components.
Here's what I get from the console:
FAIL pageComponents/AsyncPage/AsyncPage.test.tsx
β Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
β’ If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
β’ If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
β’ To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
β’ If you need a custom transformation specify a "transform" option in your config.
β’ If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
/Users/MyUser/Projects/nextjs/node_modules/jose/dist/browser/index.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){export { compactDecrypt } from './jwe/compact/decrypt.js';
^^^^^^
SyntaxError: Unexpected token 'export'
16 | }
17 | interface RawOptions extends MyFetchOptions {
> 18 | returnType: 'raw';
| ^
19 | }
20 | interface BinaryOptions extends MyFetchOptions {
21 | returnType: 'binary';
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1496:14)
at Object.<anonymous> (node_modules/openid-client/lib/client.js:8:14)
at Object.<anonymous> (node_modules/openid-client/lib/issuer.js:5:19)
at Object.<anonymous> (node_modules/openid-client/lib/index.js:1:118)
at Object.<anonymous> (node_modules/next-auth/core/lib/oauth/callback.js:8:21)
at Object.<anonymous> (node_modules/next-auth/core/routes/callback.js:10:40)
at Object.<anonymous> (node_modules/next-auth/core/routes/index.js:39:40)
at Object.<anonymous> (node_modules/next-auth/core/index.js:14:38)
at Object.<anonymous> (node_modules/next-auth/next/index.js:10:13)
at Object.<anonymous> (node_modules/next-auth/index.js:28:37)
at Object.<anonymous> (helpers/myFetch.ts:18:19)
at Object.<anonymous> (services/dataService.ts:21:24)
at Object.<anonymous> (pageComponents/AsyncPage/PanelWithLoading.tsx:20:17)
at Object.<anonymous> (pageComponents/AsyncPage/AsyncPage.tsx:21:27)
at Object.<anonymous> (pageComponents/AsyncPage/AsyncPage.test.tsx:6:22)
A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 1.746 s
Ran all test suites matching /pageComponents\/AsyncPage\/AsyncPage.test.tsx/i.
A realistic integration test with proper async child components needs a React server, and I'm working on one for React Testing Library. Until then, you can mock them or use end to end tests (Selenium, Cypress, Playwright, etc.).
As for the ES modules, that appears to be an unrelated issue. Please review the Jest docs if you want to use native Node ES modules, otherwise the Babel plugin tends to work well with older React apps.
@nickmccurdy thank you for the insights
Hey all! I just wanted to share my solution to this, as I've been running into the same limitation with async/server components.
In my case, I only have async components for the first two levels of hierarchy, then it's client components the rest of the way down the tree. I leveraged this assumption to create a recursive function which evaluates each server component until the first client component is reached.
I absolutely do not expect this solution to be comprehensive, and I wouldn't be surprised if it breaks when used in your own project. But I just wanted to offer it as something that worked for me.
// renderServerComponent.tsx
import { act, render } from '@testing-library/react';
// Return true if the supplied value is an async function; otherwise, return
// false
function isAsyncFunction(value: any): boolean {
return Object.prototype.toString.call(value) === '[object AsyncFunction]';
}
// Retrieve the nearest (i.e. outermost) client component in the component tree
// represented by the given JSX node
async function getNearestClientComponent(node: JSX.Element) {
if (!isAsyncFunction(node.type)) {
return node;
}
const nodeReturnValue = await node.type({ ...node.props });
return getNearestClientComponent(nodeReturnValue);
}
// Follow <https://github.com/testing-library/react-testing-library/issues/1209>
// for the latest updates on React Testing Library support for React Server
// Components (RSC)
export async function renderServerComponent(node: JSX.Element) {
await act(async () => {
render(await getNearestClientComponent(node));
});
}
And then to use:
// Home.test.tsx
import { renderServerComponent } from './renderServerComponent';
import Home from '../app/Home';
test('should do this', async () => {
await renderServerComponent(<Home />);
});
I believe React supports any rendered promises, not just from async functions. This probably would exclude normal functions returning promises from other async functions, for example.
Im not sure if this is implemented in Jest or RTL. But you can simple do this too.
import { render, fireEvent } from "@testing-library/react";
import Home from "../app/page";
describe("Home", () => {
test("clicking on Docs link takes user to the right page", () => {
const { getByRole, getByText } = render(<Home />);
// Locate the link using an accessible name
const docsLink = getByRole("link", { name: /Docs ->/i });
fireEvent.click(docsLink);
// You can add an assertion to check the URL or other behaviors
});
});
and snapshot
import { render } from "@testing-library/react";
import Home from "../app/page";
it("renders homepage unchanged", () => {
const { container } = render(<Home />);
expect(container).toMatchSnapshot();
});
repo: https://github.com/stephyswe/nextjs-app-jest
Update: added renderServerComponents in tests folder - not sure how the async data should look in Page.tsx
@caleb531
Hey all! I just wanted to share my solution to this, as I've been running into the same limitation with async/server components.
Can you share a repo too?
Demo
Thanks for the Demo, it works for me except for my server components marked with the
import 'server-only';
Changing environment in the vite.config.ts from jsdom
to
environment: 'node',
didn't suffice. Thoughts?
Hey all! I just wanted to share my solution to this, as I've been running into the same limitation with async/server components.
In my case, I only have async components for the first two levels of hierarchy, then it's client components the rest of the way down the tree. I leveraged this assumption to create a recursive function which evaluates each server component until the first client component is reached.
I absolutely do not expect this solution to be comprehensive, and I wouldn't be surprised if it breaks when used in your own project. But I just wanted to offer it as something that worked for me.
// renderServerComponent.tsx import { act, render } from '@testing-library/react'; // Return true if the supplied value is an async function; otherwise, return // false function isAsyncFunction(value: any): boolean { return Object.prototype.toString.call(value) === '[object AsyncFunction]'; } // Retrieve the nearest (i.e. outermost) client component in the component tree // represented by the given JSX node async function getNearestClientComponent(node: JSX.Element) { if (!isAsyncFunction(node.type)) { return node; } const nodeReturnValue = await node.type({ ...node.props }); return getNearestClientComponent(nodeReturnValue); } // Follow <https://github.com/testing-library/react-testing-library/issues/1209> // for the latest updates on React Testing Library support for React Server // Components (RSC) export async function renderServerComponent(node: JSX.Element) { await act(async () => { render(await getNearestClientComponent(node)); }); }And then to use:
// Home.test.tsx import { renderServerComponent } from './renderServerComponent'; import Home from '../app/Home'; test('should do this', async () => { await renderServerComponent(<Home />); });
I am using this approach. Thank you! Did you encounter an issue when testing an async component that contains another async component? I'm currently struggling with that. I get the same error of not being able to render Promises for this nested server component. If I remove the async keyword from it, it works.
Objects are not valid as a React child (found: [object Promise]). If you meant to render a collection of children, use an array instead.
@aurorawalberg I believe my solution will only work if your async components are at the root of each JSX tree. Because, if you look over my algorithm, it's only looking at the first immediate child of each level of the treeβI'm not traversing every single child all the way down. So if you have sibling async components, or an async component within a raw div (where the div is the root of that tree), then you will end up with some unresolved async components.
I did make a few attempts to traverse every node in the tree and convert them all to non-async components, but it ended up being too complicated. So I leveraged a few assumptions about my particular codebase to write a simpler algorithm (but that means it won't be a one-size-fits-all solution).
@aurorawalberg Please refer to the current progress, workarounds, and demo.
Thanks for the Demo, it works for me except for my server components marked with the
import 'server-only';Changing environment in the vite.config.ts from
jsdom
toenvironment: 'node',didn't suffice. Thoughts?
@realSergiy Some React Server Component libraries depend on the react-server
import condition used in server environments. server-only
is just a shortcut for throwing an error unless it's used. Unfortunately you can't use this condition with React Testing Library yet because it only has a client environment, and switching to server would disable act
which we need to test components.
If you're using Jest or Vitest, you can disable the module's error with an empty mock script named __mocks__/server-only
.
With Vitest, you'll also need to manually register it:
vi.mock("server-only");
Alternatively you can mock the module in a setup or test file, for example:
jest.mock("server-only", () => ({}));
vi.mock("server-only", () => ({}));
Ideally we might add separate client and server environments to renderServer
, which would solve this issue and provide a more realistic test setup.
Given a component like this
import '@/styles/globals.scss';
import { Inter } from 'next/font/google';
import { AbstractIntlMessages, NextIntlClientProvider } from 'next-intl';
const inter = Inter({ subsets: ['latin'] });
const supportedLocales = ['en'];
export function generateStaticParams() {
return supportedLocales.map((loc) => ({ locale: loc }));
}
export default async function RootLayout({
children,
params,
}: {
children: React.ReactNode;
params?: Record<string, string>;
}) {
const locale =
params?.locale && supportedLocales.includes(params.locale)
? params.locale
: 'en';
const { default: messages } = (await import(
`../../public/locales/${locale}.json`
)) as { default: AbstractIntlMessages };
return (
<html lang={locale}>
<body className={inter.className}>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
</NextIntlClientProvider>
</body>
</html>
);
}
so, this works - but is really no great:
describe('RootLayout tests', () => {
it('renders layout', async () => {
const component = await RootLayout(
<div>
<span>hello</span>
</div>,
);
render(component);
const bodyElement = screen.getByRole('document');
expect(bodyElement).toBeInTheDocument();
});
});
Hi, Has there been any updates on how to test server side components correctly?
Currently getting this issue on a test
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
test('Async component', async () => {
render(await AsyncComponent());
});
@khaleelkhalifa It sounds like you have a child component that's also async. Rendering React Server Components isn't supported in stable React. I'd recommend following my workarounds, which includes a solution that should fix this issue using React canary, React Testing Library, and your preferred test framework. If you have any additional issues, please let us know.
I've been encountering an interesting issue related to this and was wondering if maybe I could get some help. I have some code I am trying to test using Jest. When I run the app (Next.JS) in dev mode, it works just fine, but when I use it in my Jest test, I get, Warning: async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding 'use client' to a module that was originally written for the server.
The test is extremely simple, although I am using Mantine's workaround for Jest
Am I doing something wrong? All of my tests with client components only work. If I comment out the <Welcome />
server component it works as well.
it('opens the create business modal for new users', async () => {
render(await Dashboard());
expect(await screen.findByText('Welcome!')).toBeInTheDocument();
expect(await screen.findByRole('dialog')).toBeInTheDocument();
});
export default async function Dashboard() {
const user = await fetchUserDetails(); // server action mocked in test
const business = await fetchBusinessDetails(); // server action mocked in test
return (
<>
<Welcome /> // SERVER COMPONENT
<main className="relative -mt-12 pb-12">
<Container size={1280}>
<BoxedStatistics /> // CLIENT COMPONENT
...
</Container>
</main>
</>
);
}
But your test still passes, right? This is expected behavior, as React's client side rendering for server components doesn't appear to be considered stable.
But your test still passes, right? This is expected behavior, as React's client side rendering for server components doesn't appear to be considered stable.
The test does not pass; it errors. It passes if I comment out the <Welcome />
server component, though. Maybe this is more of a Next.js bug I should report there.
Warnings generally don't throw, so I think you have an unrelated failure in your test. You're welcome to report with Next or here, though I'll hide our discussion after it's concluded if it isn't related to server components.
@stephyswe #1209 (comment)
There is another problem. If you export generateMetadata
in the /app/page.tsx
it won't work and you will get the following error:
FAIL app/page.test.tsx
β Test suite failed to run
Γ NEXT_RSC_ERR_CLIENT_METADATA_EXPORT: generateMetadata
ββ[/Users/.../my-app/app/page.tsx:3:1]
3 β
4 β export const revalidate = 180
5 β
6 β export const generateMetadata = ({ params }) => {
Β· ββββββββββββββββ
7 β
8 β const Page = (props) => {
β°ββββ
3 | describe('Homepage', () => {
4 | it('should have a title', async () => {
> 5 | const metadata = await generateMetadata({ params: { slug: '/' } })
| ^
6 |
7 | expect(metadata.title).toBeDefined()
8 | })
Hi! Is there an issue to render a tree of async components? e.g. I'm writing tests for a component that's Built up like this:
<ErrorBoundary>
<Suspense>
<AsyncComponent />
</Suspense>
</ErrorBoundary>
And jsdom always throws an error about Promises not being a valid React child.
Objects are not valid as a React child (found: [object Promise])
Is there anyone that has had the same and found a possible workaround?
Hi! Is there an issue to render a tree of async components? e.g. I'm writing tests for a component that's Built up like this:
<ErrorBoundary> <Suspense> <AsyncComponent /> </Suspense> </ErrorBoundary>And jsdom always throws an error about Promises not being a valid React child.
Objects are not valid as a React child (found: [object Promise])
Is there anyone that has had the same and found a possible workaround?
My current workaround is to mock the child async components and test them separately.
vi.mock('@/components/header/UserProfile', () => {
return {
__esModule: true,
default: () => {
return <div />;
},
};
});
We don't recommend mocking components like this, as it prevents you from testing against the original implementation of your component. Instead, make sure you're using React 18.3 (canary) in your tests.
We don't recommend mocking components like this, as it prevents you from testing against the original implementation of your component. Instead, make sure you're using React 18.3 (canary) in your tests.
Thanks, I am aware it's not best practice, it's just a workaround for testing these trees of async components with child async components. I couldn't get it working with the React canary version the last time I tried. I'll give it another shot.
@aurorawalberg @robinvandenb Please review the workarounds if you haven't recently. Let me know if you have any issues. I'll try to update the demo.
The React team has announced that server components will likely be released in version 19, not 18.3, so we may want to merge issues involving both server components and the breaking changes in 19.
As for my work, I'm still focused on updating our current progress, workarounds, and demo along with related resources like Are we RSC yet? and react-unpin
. I don't have time to continue working on renderServer
yet, but it's still a longer team goal of mine.
@nickmccurdy @Gpx do you have any clue on how to tests an async RSC that has another async RSC nested inside? Seems that the workaround(no React canary with suspense) works for the top level async RSC, but if I try to add another component that uses the async keyword, then the error is being shown again:
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.]
The example structure would look something like
export async function MyRSCParent(){
const data = await fetch(....)
return <MyRSCChild />
}
async function MyRSCChild() {
const otherData = await fetch(...)
return <span>{otherData}</span<
}
I can move the fetch from the child to the parent and pass it as props, but would be very nice if every server component could have its own fetch without having to centralize it in the top level
@Ruffeng Please use the workarounds, they properly use React canary to support nested components. Anything else will require reimplementing async components, which is not worth it for Testing Library or any end user to support.
@nickmccurdy thanks for your quick answer. So far, I moved the fetch functionality into the top level of my RSC and then passed it via props with non-async server components. I cannot use the Canary version because this code is for the company I am working for, and they are not willing to host a react version that might not be stable yet.
I will keep an eye on the new React release, and I will switch it back as soon as it's released π
@Ruffeng If you're using server components, you are already using React canary, all you need is to install it for Testing Library. If you're concerned about breaking changes, you can use react-unpin
to install the same version as your framework. Also, using canary releases in production for this use case is totally safe, but skipping testing is not will lead to more regressions.