Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NEXT-863] Unable to test page components using metadata API with Jest #47299

Closed
1 task done
andremendonca opened this issue Mar 19, 2023 · 43 comments · Fixed by #53578 or #54891
Closed
1 task done

[NEXT-863] Unable to test page components using metadata API with Jest #47299

andremendonca opened this issue Mar 19, 2023 · 43 comments · Fixed by #53578 or #54891
Labels
area: app App directory (appDir: true) locked Metadata Related to Next.js' Metadata API. Testing Related to testing with Next.js.

Comments

@andremendonca
Copy link

andremendonca commented Mar 19, 2023

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: arm64
      Version: Darwin Kernel Version 22.3.0: Mon Jan 30 20:38:37 PST 2023; root:xnu-8792.81.3~2/RELEASE_ARM64_T6000
    Binaries:
      Node: 18.12.1
      npm: 8.19.2
      Yarn: 1.22.19
      pnpm: N/A
    Relevant packages:
      next: 13.2.5-canary.8
      eslint-config-next: N/A
      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)

Link to the code that reproduces this issue

https://github.com/andremendonca/metadata-jest-error

To Reproduce

git clone https://github.com/andremendonca/metadata-jest-error.git
npm i
npm test

Describe the Bug

in NextJs 13+ using the experimental App folder, I cannot test any server component that exports metadata or generateMetadata from https://beta.nextjs.org/docs/api-reference/metadata

Resulting in the following error:

 × NEXT_RSC_ERR_CLIENT_METADATA_EXPORT: metadata
       ╭─[/metadata-jest-bug/app/page.tsx:1:1]
     1 │ export const metadata = {
       ·              ────────
     2 │   title: 'My jest sample'
     3 │ }
       ╰────

Expected Behavior

I expected to be able to test Server Components present on app directory

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

From SyncLinear.com | NEXT-863

@andremendonca andremendonca added the bug Issue was opened via the bug report template. label Mar 19, 2023
@balazsorban44 balazsorban44 added area: app App directory (appDir: true) Testing Related to testing with Next.js. Metadata Related to Next.js' Metadata API. area: documentation labels Mar 21, 2023
@balazsorban44
Copy link
Member

Hi, the issue is that @testing-library/react simulates a browser environment, while metadata is intended to run on the server only.

I will discuss with the team what we should recommend in this case, but if you want to test a page, you could likely use Playwright or a similar tool to spin up a server where metadata can run correctly.

Another solution could be to extract the Page component into a non-page file and import it into Jest from there, and re-export it in your page file.

@balazsorban44 balazsorban44 added type: needs investigation and removed bug Issue was opened via the bug report template. labels Mar 21, 2023
@balazsorban44 balazsorban44 changed the title Unable to test page components using metadata API with Jest [NEXT-863] Unable to test page components using metadata API with Jest Mar 21, 2023
@CedericPrivat
Copy link

Hi, the issue is that @testing-library/react simulates a browser environment, while metadata is intended to run on the server only.

I will discuss with the team what we should recommend in this case, but if you want to test a page, you could likely use Playwright or a similar tool to spin up a server where metadata can run correctly.

Another solution could be to extract the Page component into a non-page file and import it into Jest from there, and re-export it in your page file.

I'm having the same issue. Are there any updates on recommendations for testing server components?

@red2678
Copy link

red2678 commented Apr 9, 2023

Same issue.

@JeStew
Copy link

JeStew commented Apr 18, 2023

Also having this issue

@kameikay
Copy link

Same issue.

@RafaelZasas
Copy link

same issue

1 similar comment
@pravtz
Copy link

pravtz commented May 12, 2023

same issue

@NebraskaCoder
Copy link

Yup. Same.

@DonikaV
Copy link

DonikaV commented May 25, 2023

So, is it any way to have tests with new app folder?

@red2678

This comment was marked as spam.

@phdduarte
Copy link

same issue

@DonikaV
Copy link

DonikaV commented May 31, 2023

Anybody found a workaround ?

@fuyu77
Copy link

fuyu77 commented Jun 9, 2023

I am writing the tests as follows: creating a wrapper server component for the Page component and writing Jest tests for it.

// app/products/[id]/product-page.tsx

import type { Product } from '@/lib/api/interfaces/product';

interface Props {
  product: Product;
}

export default function ProductPage({ product }: Props) {
  return (
    <div>{product.name}</div>
  );
}
// app/products/[id]/page.tsx

import ProductPage from './product-page';

interface Params {
  params: {
    id: string;
  };
}

export async function generateMetadata({ params }: Params) {
  const product = await fetch('https://api.example.com/...');
  const title = product.name;
  return { title };
}

export default async function Page({ params }: Params) {
  const product = await fetch('https://api.example.com/...');

  return <ProductPage product={product} />;
}
// __tests__/app/products/[id]/product-page.test.tsx

import { render, screen } from '@testing-library/react';
import ProductPage from '@/app/products/[id]/product-page';

describe('ProductPage', () => {
  const product = { id: 1, name: 'product name' };

  test('renders props', () => {
    render(<ProductPage product={product} />);

    expect(screen.getByText(product.name)).toBeInTheDocument();
  });
});

@ctsstc
Copy link

ctsstc commented Jun 15, 2023

I was trying to write my first test in a brand new repository and it's a pretty sour taste in the mouth and frustrating when the first test you write blows up and SWC gives you no useful message to go off of. I also don't understand why the arrows for the error messages seem to point to nothing useful or relevant. Am I doing something wrong to get such an ugly useless stack trace, or is this just how SWC works; are people fine with this useless ugliness?¿¿ I love the speed that SWC seems to bring, but if it creates useless errors, it makes me not want it in my tooling if it's providing a bad DX.

Minimum Reproduction

Config

This seems to be irrelevant, but here's what I've got going on. I did this so that I could have the test next to the source file/page.

/** @type {import('next').NextConfig} */
const nextConfig = {
  pageExtensions: ['page.tsx', 'page.ts', 'page.jsx', 'page.js'],
}

module.exports = nextConfig

Directory Structure

./src/app/social/[userHandle]
├── page.test.tsx
└── page.tsx

page.test.tsx

Just an irrelevant test. It seems to get angry that the generateMetadata exists as an export from the imported file.

import { add } from './page'

describe('Page', () => {
  describe('#add', () => {
    it('adds two numbers', () => {
      expect(add(1, 3)).toEqual(4)
    })
  })
})

page.tsx

Note: This is just a minimal reproduction but the real code has some async work going on here.

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  return {
    title: 'title',
    description: 'description',
  }
}

export default function SocialPage({ params }: Props) {
  return <h1>Hello Worlds</h1>
}

export function add(num1: number, num2: number) {
  return num1 + num2
}

@alfirmanpahlepi
Copy link

same issue

@kevmarchant
Copy link

Same here, anyone got any ideas?

@ctsstc
Copy link

ctsstc commented Jun 20, 2023

I ended up using Cypress to test this, but checking the exported function should be trivial.

I know at the end of the day what I'm actually interested in is the outcome of the rendered page so Cypress makes sense. On the other hand that should be part of an implementation that's outside of the scope of my code (it belongs to Next JS, and I'll [blind] trust that the dependency I'm using is doing what I expect it to do).

I wasn't originally planning on using Cypress in this project and it added some overhead, but it was nice to give a try since I've been eyeballing it for many years but never tried it out.

@feugy
Copy link
Member

feugy commented Jul 24, 2023

@philwolstenholme unfortunately, #52393 only helps with testing server only code, like the one you may have in libs.
It does not help with testing RSC.

@inakianduaga
Copy link

It's really dissapointing that this bug is still on after 4 months. generateMetadata is a built in feature from Nextjs, and using jest for testing components is really basic. The fact that the app directory was put on stable and there are bugs like this (alongside i18n support breaking draft mode and other things) puzzles me.

@8lane
Copy link

8lane commented Jul 31, 2023

Using RTL for integration testing is very common. Having to use Cypress as a workaround is a real shame

@justyn-clark
Copy link

Same here 👋🏼

@soutot
Copy link

soutot commented Aug 9, 2023

I'm using this workaround for now. It doesn't look good, but so far it seems to work

./src/app/(home)/
├── __tests__
  └──  _component.test.tsx
├── _component.tsx
└── page.tsx
// page.tsx
import {Metadata} from 'next'
import Component from './_component'

export const metadata: Metadata = {
  title: "Foo",
  description: "Bar",
}

export default function Home() {
  return <Component />
}
// _component.tsx
export default function Component() {
  return <div data-testid="myid">Hello world</div>
}
// __tests__/_component.test.tsx
import '@testing-library/jest-dom'
import {render, screen} from '@testing-library/react'
import Component from '../_component'

describe('Home', () => {
  it('renders an element', () => {
    render(<Component />)
    const element = screen.getByTestId("myid")

    expect(element).toBeInTheDocument()
  })
})

@kodiakhq kodiakhq bot closed this as completed in #53578 Aug 16, 2023
kodiakhq bot pushed a commit that referenced this issue Aug 16, 2023
…nt (#53578)

### 🧐 What's in there?

This is another attempt to allow testing server-only code with Jest.

### 🧪 How to test?

There's an integration tests which can be triggered with `pnpm testheadless server-only`

Here is a more comprehensive setup:
<details>
<summary><code>app/lib/index.ts</code></summary>

```ts
import 'server-only'

export function add(num1: number, num2: number) {
  return num1 + num2
}
```
</details>
<details>
<summary><code>app/lib/index.test.ts</code></summary>

```ts
import { add } from '.'

it('adds two numbers', () => {
  expect(add(1, 3)).toEqual(4)
})
```
</details>
<details>
<summary><code>app/client-component.tsx</code></summary>

```ts
'use client'

import { useState } from 'react'

export default function ClientComponent() {
  const [text, setText] = useState('not clicked yet')
  return <button onClick={() => setText('clicked!')}>{text}</button>
}
```
</details>
<details>
<summary><code>app/client-component.test.tsx</code></summary>

```ts
import { fireEvent, render, screen } from '@testing-library/react'
import ClientComponent from './client-component'

it('can be clicked', async () => {
  render(<ClientComponent />)
  const button = screen.getByRole('button')
  expect(button).toHaveTextContent('not clicked yet')
  await fireEvent.click(button)
  expect(button).toHaveTextContent('clicked!')
})
```
</details>
<details>
<summary><code>app/server-component.tsx</code></summary>

```ts
import { add } from '@/lib'

export default function ServerComponent({ a, b }: { a: number; b: number }) {
  return (
    <code role="comment">
      {a} + {b} = {add(a, b)}
    </code>
  )
}
```
</details>
<details>
<summary><code>app/server-component.test.tsx</code></summary>

```ts
import { render, screen } from '@testing-library/react'
import ServerComponent from './server-component'

it('renders', () => {
  render(<ServerComponent a={2} b={3} />)
  expect(screen.getByRole('comment')).toHaveTextContent('2 + 3 = 5')
})
```
</details>
<details>
<summary><code>app/page.tsx</code></summary>

```ts
import Link from 'next/link'
import ClientComponent from './client-component'
import ServerComponent from './server-component'

export default function Page() {
  return (
    <>
      <h1>Hello World</h1>
      <Link href="/dave">Dave?</Link>
      <p>
        <ClientComponent />
      </p>
      <p>
        <ServerComponent a={5} b={2} />
      </p>
    </>
  )
}
```
</details>
<details>
<summary><code>app/page.test.tsx</code></summary>

```ts
import { render, screen } from '@testing-library/react'
import Page from './page'

it('greets', () => {
  render(<Page />)
  expect(screen.getByRole('link')).toHaveTextContent('Dave?')
  expect(screen.getByRole('heading')).toHaveTextContent('Hello World')
  expect(screen.getByRole('button')).toHaveTextContent('not clicked yet')
  expect(screen.getByRole('comment')).toHaveTextContent('5 + 2 = 7')
})
```
</details>
<details>
<summary><code>app/[blog]/page.tsx</code></summary>

```ts
import { Metadata } from 'next'
import Link from 'next/link'

type Props = {
  params: { blog: string }
}

export async function generateMetadata({
  params: { blog: title },
}: Props): Promise<Metadata> {
  return { title, description: `A blog post about ${title}` }
}

export default function Page({ params }: Props) {
  return (
    <>
      <div>
        <Link href="/">Back</Link>
      </div>
      <h1>All about {params.blog}</h1>
    </>
  )
}
```
</details>
<details>
<summary><code>app/[blog]/page.test.tsx</code></summary>

```ts
import { render, screen } from '@testing-library/react'
import Page from './page'

it('has the appropriate title', () => {
  const title = 'Jane'
  render(<Page params={{ blog: title }} />)
  expect(screen.getByRole('heading')).toHaveTextContent(`All about ${title}`)
  expect(screen.getByRole('link')).toHaveTextContent('Back')
})
```
</details>
<details>
<summary><code>app/layout.tsx</code></summary>

```ts
export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}
```
</details>
<details>
<summary><code>jest.config.js</code></summary>

```ts
const nextJest = require('next/jest')

const createJestConfig = nextJest({ dir: './' })

module.exports = createJestConfig({
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/test-setup.ts'],
})
```
</details>
<details>
<summary><code>package.json</code></summary>

```ts
{
  "name": "rsc-test",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "test": "jest"
  },
  "devDependencies": {
    "@testing-library/jest-dom": "latest"
  }
}
```
</details>
<details>
<summary><code>test-setup.ts</code></summary>

```ts
import '@testing-library/jest-dom'
```
</details>
   
The app should run and all test should pass.

### ❗ Notes to reviewers

#### The problem:

1. next/jest configures jest with a transformer ([jest-transformer](https://github.com/vercel/next.js/blob/canary/packages/next/src/build/swc/jest-transformer.ts)) to compile react code with next -swc
2. the transformers configures next -swc for a given environment: Server or Client, based on jest global environment
3. Based on the environment, next -swc checks for invalid usage of `import('server-only')`  `“use client”`, `export const metadata` or `export async function generateMetadata` 
4. Because the global test environment is either jsdom or node, the same test suite can not include both client and server components

#### Possible mitigations

*A. Using jest projects*

When configured with [multiple projects](https://jestjs.io/docs/next/configuration/#projects-arraystring--projectconfig), Jest can launch different runners with different environment. This would allow running server-only code in node and client-only code in jsdom. 

However, it requires user to completely change their jest configuration. It would also require a different setup when scaffolding new app-directory project with create-next.

*B. Using doc blocks*

Jest allows changing the environment per test file [with docBlock](https://jestjs.io/docs/configuration#testenvironment-string). 

However, by the time jest is invoking next -swc on a source file to transform it, this information is gone, and next -swc is still invoked with the (wrong) global environment.

The PR #52393 provides a workaround for files with `import('server-only')`, but does not allow testing pages with metadata.

*C. Always compile for node*

Our jest-transformer could always configure next -swc for server:

- pass Server-specific validations `import('server-only')`  `export const metadata` or `export async function generateMetadata`
- does not complain about `"use client"`

This is what this PR is about!

Fixes #47299

Co-authored-by: Jiachi Liu <4800338+huozhi@users.noreply.github.com>
@pongbao
Copy link

pongbao commented Aug 16, 2023

Same issue, but only occurs on Github Actions; works on my local machine otherwise

image

jobs:
  simple_deployment_pipeline:
    name: Lint and install dependencies
    runs-on: ubuntu-20.04
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: "18"
      - name: Install dependencies on frontend
        working-directory: ./
        run: npm install
      - name: Run jest tests
        run: npm run test:ci

@huozhi huozhi reopened this Aug 17, 2023
@nickserv
Copy link
Contributor

Did you commit a lockfile update with the patched version of Next?

@theboyknowsclass
Copy link

i did , but i still have the same issue on my repo -> CraftBeerCares/craftbeercares.github.io#9

@theboyknowsclass
Copy link

what version are we meant to be using?

@JamesBender
Copy link

It's really dissapointing that this bug is still on after 4 months. generateMetadata is a built in feature from Nextjs, and using jest for testing components is really basic. The fact that the app directory was put on stable and there are bugs like this (alongside i18n support breaking draft mode and other things) puzzles me.

I 100% agree here. I only considered version 13 for use on client work because the app directory was marked as stable. Being able to write tests isn't something I consider optional, and I have a hard enough time getting other developers to get on board with testing without having to have a workaround for this. The "... just use Playwrite" response is not sufficient. I REALLY like this framework, but it's difficult for me to promote it to my client with this issue unresolved.

@nickserv
Copy link
Contributor

what version are we meant to be using?

@theboyknowsclass You appear to be using the patched version, not sure why this is still broken.

@italogama

This comment was marked as off-topic.

@theboyknowsclass
Copy link

what version are we meant to be using?

@theboyknowsclass You appear to be using the patched version, not sure why this is still broken.

mine is a client-only app.... no server-side code, seems to be an issue with that?

@leerob
Copy link
Member

leerob commented Sep 4, 2023

Hey everyone, we have a fix landed on the latest canary (v13.4.20-canary.16). Please let us know if it's working for y'all 🙏 Appreciate your patience.

@mruba
Copy link

mruba commented Sep 4, 2023

v13.4.20-canary.16

it works! thank you so much

ppywmjr added a commit to ppywmjr/mike-james-rust-personal-site that referenced this issue Sep 4, 2023
Nextjs meta data makes page test fail as meta data only runs on server and react tests run as client only
See vercel/next.js#47299
ppywmjr added a commit to ppywmjr/mike-james-rust-personal-site that referenced this issue Sep 4, 2023
* feat: add menu to home page

* fix: upgrade next for remote test fix
Nextjs meta data makes page test fail as meta data only runs on server and react tests run as client only
See vercel/next.js#47299
@leerob
Copy link
Member

leerob commented Sep 5, 2023

We're also updating examples here: #54989

bBankert pushed a commit to bBankert/next-resume that referenced this issue Sep 7, 2023
There was an issue that was affecting metadata tests with server side components.
Fix linked here: vercel/next.js#47299
bBankert added a commit to bBankert/next-resume that referenced this issue Sep 7, 2023
* Add CI workflow

* Run on all branches with changes to code files

* Test commit

* Remove test console, add yml file externsion, and remove 16

* Add generation of metadata and re-add 16

* Bump next package to the version fix

There was an issue that was affecting metadata tests with server side components.
Fix linked here: vercel/next.js#47299

* Add pull request triggers as well

* Fix typo & schema

---------

Co-authored-by: Brandon Bankert <bankert.brandon@gmail.com>
@github-actions
Copy link
Contributor

This closed issue has been automatically locked because it had no new activity for 2 weeks. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 19, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area: app App directory (appDir: true) locked Metadata Related to Next.js' Metadata API. Testing Related to testing with Next.js.
Projects
None yet