next.js/test/development/acceptance-app/server-components.test.ts
server-components.test.ts569 lines18.3 KB
/* eslint-env jest */
import { createSandbox } from 'development-sandbox'
import { FileRef, nextTestSetup } from 'e2e-utils'
import path from 'path'
import { check } from 'next-test-utils'
import { outdent } from 'outdent'

describe('Error Overlay for server components', () => {
  const { next } = nextTestSetup({
    files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')),
    skipStart: true,
  })

  describe('createContext called in Server Component', () => {
    it('should show error when React.createContext is called', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'app/page.js',
            outdent`
              import React from 'react'
              const Context = React.createContext()
              export default function Page() {
                return (
                  <>
                      <Context.Provider value="hello">
                          <h1>Page</h1>
                      </Context.Provider>
                  </>
                )
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component'
      )
    })

    it('should show error when React.createContext is called in external package', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'node_modules/my-package/index.js',
            outdent`
              const React = require('react')
              module.exports = React.createContext()
            `,
          ],
          [
            'node_modules/my-package/package.json',
            outdent`
              {
                "name": "my-package",
                "version": "0.0.1"
              }
            `,
          ],
          [
            'app/page.js',
            outdent`
              import Context from 'my-package'
              export default function Page() {
                return (
                  <>
                      <Context.Provider value="hello">
                          <h1>Page</h1>
                      </Context.Provider>
                  </>
                )
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component'
      )
    })

    it('should show error when createContext is called in external package', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'node_modules/my-package/index.js',
            outdent`
              const { createContext } = require('react')
              module.exports = createContext()
            `,
          ],
          [
            'node_modules/my-package/package.json',
            outdent`
              {
                "name": "my-package",
                "version": "0.0.1"
              }
            `,
          ],
          [
            'app/page.js',
            outdent`
              import Context from 'my-package'
              export default function Page() {
                return (
                  <>
                      <Context.Provider value="hello">
                          <h1>Page</h1>
                      </Context.Provider>
                  </>
                )
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'createContext only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/context-in-server-component'
      )
    })
  })

  describe('React component hooks called in Server Component', () => {
    it('should show error when React.<client-hook> is called', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'app/page.js',
            outdent`
              import React from 'react'
              export default function Page() {
                const ref = React.useRef()
                return "Hello world"
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'useRef only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'useRef only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
      )
    })

    it('should show error when React.experiment_useOptimistic is called', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'app/page.js',
            outdent`
              import React from 'react'
              export default function Page() {
                const optimistic = React.experimental_useOptimistic()
                return "Hello world"
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'experimental_useOptimistic only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'experimental_useOptimistic only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
      )
    })

    it('should show error when React.experiment_useOptimistic is renamed in destructuring', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'app/page.js',
            outdent`
              import { experimental_useOptimistic as useOptimistic } from 'react'
              export default function Page() {
                const optimistic = useOptimistic()
                return "Hello world"
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        const html = await browser.eval('document.documentElement.innerHTML')
        expect(html).toContain('experimental_useOptimistic')
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain('experimental_useOptimistic')
    })

    it('should show error when React.<client-hook> is called in external package', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'node_modules/my-package/index.js',
            outdent`
              const React = require('react')
              module.exports = function Component() {
                const [state, useState] = React.useState()
                return "Hello world"
              }
            `,
          ],
          [
            'node_modules/my-package/package.json',
            outdent`
              {
                "name": "my-package",
                "version": "0.0.1"
              }
            `,
          ],
          [
            'app/page.js',
            outdent`
              import Component from 'my-package'
              export default function Page() {
                return <Component />
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'useState only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
      )
    })

    it('should show error when React client hook is called in external package', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'node_modules/my-package/index.js',
            outdent`
              const { useEffect } = require('react')
              module.exports = function Component() {
                useEffect(() => {}, [])
                return "Hello world"
              }
            `,
          ],
          [
            'node_modules/my-package/package.json',
            outdent`
              {
                "name": "my-package",
                "version": "0.0.1"
              }
            `,
          ],
          [
            'app/page.js',
            outdent`
              import Component from 'my-package'
              export default function Page() {
                return <Component />
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'useEffect only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'useEffect only works in Client Components. Add the "use client" directive at the top of the file to use it. Read more: https://nextjs.org/docs/messages/react-client-hook-in-server-component'
      )
    })
  })

  describe('Class component used in Server Component', () => {
    it('should show error when Class Component is used', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'app/page.js',
            outdent`
              import React from 'react'
              export default class Page extends React.Component {
                render() {
                  return <p>Hello world</p>
                }
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'This might be caused by a React Class Component being rendered in a Server Component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'This might be caused by a React Class Component being rendered in a Server Component'
      )
    })

    it('should show error when React.PureComponent is rendered in external package', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'node_modules/my-package/index.js',
            outdent`
              const React = require('react')
              module.exports = class extends React.PureComponent {
                render() {
                  return "Hello world"
                }
              }
            `,
          ],
          [
            'node_modules/my-package/package.json',
            outdent`
              {
                "name": "my-package",
                "version": "0.0.1"
              }
            `,
          ],
          [
            'app/page.js',
            outdent`
              import Component from 'my-package'
              export default function Page() {
                return <Component />
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'This might be caused by a React Class Component being rendered in a Server Component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'This might be caused by a React Class Component being rendered in a Server Component'
      )
    })

    it('should show error when Component is rendered in external package', async () => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'node_modules/my-package/index.js',
            outdent`
              const { Component } = require('react')
              module.exports = class extends Component {
                render() {
                  return "Hello world"
                }
              }
            `,
          ],
          [
            'node_modules/my-package/package.json',
            outdent`
              {
                "name": "my-package",
                "version": "0.0.1"
              }
            `,
          ],
          [
            'app/page.js',
            outdent`
              import Component from 'my-package'
              export default function Page() {
                return <Component />
              }
            `,
          ],
        ])
      )
      const { browser } = sandbox
      await check(async () => {
        expect(
          await browser
            .waitForElementByCss('#nextjs__container_errors_desc')
            .text()
        ).toContain(
          'This might be caused by a React Class Component being rendered in a Server Component'
        )
        return 'success'
      }, 'success')

      expect(next.cliOutput).toContain(
        'This might be caused by a React Class Component being rendered in a Server Component'
      )
    })
  })

  describe('Next.js navigation client hooks called in Server Component', () => {
    it.each([
      ['useParams'],
      ['useRouter'],
      ['usePathname'],
      ['useSearchParams'],
      ['useSelectedLayoutSegment'],
      ['useSelectedLayoutSegments'],
    ])('should show error when %s is called', async (hook: string) => {
      await using sandbox = await createSandbox(
        next,
        new Map([
          [
            'app/page.js',
            outdent`
              import { ${hook} } from 'next/navigation'
              export default function Page() {
                return "Hello world"
              }
            `,
          ],
        ])
      )
      const { session } = sandbox
      await session.waitForRedbox()
      // In webpack when the message too long it gets truncated with `  | ` with new lines.
      // So we need to check for the first part of the message.
      const normalizedSource = await session.getRedboxSource()
      expect(normalizedSource).toContain(
        `You're importing a module that depends on \`${hook}\` into a React Server Component module. This API is only available in Client Components. To fix, mark the file (or its parent) with the \`"use client"\` directive.`
      )
      expect(normalizedSource).toContain(
        `import { ${hook} } from 'next/navigation'`
      )
    })
  })

  describe('Next.js link client hooks called in Server Component', () => {
    it.each([['useLinkStatus']])(
      'should show error when %s is called',
      async (hook: string) => {
        await using sandbox = await createSandbox(
          next,
          new Map([
            [
              'app/page.js',
              outdent`
              import { ${hook} } from 'next/link'
              export default function Page() {
                return "Hello world"
              }
            `,
            ],
          ])
        )
        const { session } = sandbox
        await session.waitForRedbox()
        // In webpack when the message too long it gets truncated with `  | ` with new lines.
        // So we need to check for the first part of the message.
        const normalizedSource = await session.getRedboxSource()
        expect(normalizedSource).toContain(
          `You're importing a module that depends on \`${hook}\` into a React Server Component module. This API is only available in Client Components. To fix, mark the file (or its parent) with the \`"use client"\` directive.`
        )
        expect(normalizedSource).toContain(
          `import { ${hook} } from 'next/link'`
        )
      }
    )
  })
})
Quest for Codev2.0.0
/
SIGN IN