vercel / next.js

The React Framework
https://nextjs.org
MIT License
126.98k stars 26.98k forks source link

Unable to test `async` components with Jest #47131

Open felipemullen opened 1 year ago

felipemullen commented 1 year ago

Verify canary release

Provide environment information

Operating System:
  Platform: darwin
  Arch: arm64
  Version: Darwin Kernel Version 21.6.0: Wed Aug 10 14:28:23 PDT 2022; root:xnu-8020.141.5~2/RELEASE_ARM64_T6000
Binaries:
  Node: 18.9.0
  npm: 8.19.1
  Yarn: N/A
  pnpm: N/A
Relevant packages:
  next: 13.2.5-canary.3
  eslint-config-next: 13.2.4
  react: 18.2.0
  react-dom: 18.2.0

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true), Jest (next/jest), TypeScript

Link to the code that reproduces this issue

https://github.com/felipemullen/async-jest-bug

To Reproduce

git clone https://github.com/felipemullen/async-jest-bug
npm i
npm test

Describe the Bug

in NextJs 13+ using the experimental App folder, async server components can be written as described by the documentation and the async RFC

These components cannot be tested with Jest and @testing-library/react. The component does not render, but instead spits an infinite loop to the console with the following error:

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.]

Expected Behavior

Jest should be able to run without error when rendering an async component when running a simple test such as

it('should render without crashing', async () => {
    // @ts-expect-error Async Component
    const { container } = await waitFor(() => render(<Page />));
    expect(container).toBeTruthy();
});

Which browser are you using? (if relevant)

None

How are you deploying your application? (if relevant)

None

felipemullen commented 1 year ago

Does anyone have a workaround for this? Currently unable to bring test coverage up to an acceptable level on any page that includes an async component

PauRUES commented 1 year ago

We have tried this and work it:

/**
 * @param {function} Component
 * @param {*} props
 * @returns {Promise<()=>JSX.Element>}
 */
async function resolvedComponent(Component, props) {
  const ComponentResolved = await Component(props)
  return () => ComponentResolved
}

describe('Header test', () => {
  it('should render all the content', async () => {
    const HeaderResolved = await resolvedComponent(Header, {
      language: 'es',
      country: 'ES',
    })
    render(<HeaderResolved />)

    expect(screen.getByText('Login')).toBeInTheDocument()
  })
})
felipemullen commented 1 year ago

@PauRUES thanks, that is helpful. It appears to solve the issue for a single async component as the root of a page. Unfortunately does not appear to solve the problem when mixing with other async child components:

export async function DataComponent() {
    const moreData = api.fetchMoreData();

    return (
        <pre>{JSON.stringify(moreData)}</pre>
    );
}

export default async function Page() {
    const data = await api.fetchData();

    return (
        <Suspense fallback={<CenteredSpinner color="text-primary" />}>
            {/* @ts-expect-error */}
            <DataComponent />
            <pre>{JSON.stringify(data)}</pre>
        </Suspense>
    );
}

is this not supported by next?

DonikaV commented 1 year ago

I am only one who getting " Uncaught [Error: invariant expected app router to be mounted]" error ?

gweinert commented 1 year ago

I am only one who getting " Uncaught [Error: invariant expected app router to be mounted]" error ?

No, I was getting that error too whenever writing tests for components that use the useRouter hook. I figured out that mocking out the useRouter hook makes the error go away. However, I found that even if a child component is using the hook, I still get the error. So I have to mock useRouter in most of my specs.

timbarclay commented 1 year ago

As @felipemullen said, I think the workaround works where you only have a single async component at the root of the tree, but if that component has a child that is also an async component - which nextjs supports fine - jest gives this error

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.]
nickserv commented 1 year ago

This issue appears to be more about issues with the test environment not supporting RSC than Next itself. You can check https://github.com/testing-library/react-testing-library/issues/1209 for information and news on testing RSCs with React Testing Library.

carlosllorca commented 3 months ago

Hi, jest's render() function doesn't accept a promise as a parameter so you need to resolve the component by calling it as a function before passing it to the function. For example: describe('A test suite for an async component',()=>{ test('Component well render ok', async()=>{ const component = await YourComponent({customProp:true}); const {debug} = render(component); debug();

} })

BenChirlinn commented 4 weeks ago

So is the work around just to mock out the problematic children components? Or is there something more elegant?