2023-06-21 21:47:21 +02:00
import { FileRef , nextTestSetup } from 'e2e-utils'
2023-02-07 20:56:03 +01:00
import { check } from 'next-test-utils'
2023-01-25 00:13:37 +01:00
import path from 'path'
2023-06-21 21:47:21 +02:00
import { sandbox } from 'development-sandbox'
import { outdent } from 'outdent'
2023-01-25 00:13:37 +01:00
2023-06-21 21:47:21 +02:00
describe ( 'Error overlay - RSC build errors' , ( ) = > {
2024-02-16 01:52:29 +01:00
const { next , isTurbopack } = nextTestSetup ( {
2023-01-25 00:13:37 +01:00
files : new FileRef ( path . join ( __dirname , 'fixtures' , 'rsc-build-errors' ) ) ,
dependencies : {
react : 'latest' ,
'react-dom' : 'latest' ,
} ,
skipStart : true ,
2023-06-21 21:47:21 +02:00
} )
it ( 'should throw an error when getServerSideProps is used' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/client-with-errors/get-server-side-props'
)
const pageFile = 'app/client-with-errors/get-server-side-props/page.js'
const content = await next . readFile ( pageFile )
const uncomment = content . replace (
'// export function getServerSideProps' ,
'export function getServerSideProps'
)
await session . patch ( pageFile , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
'"getServerSideProps" is not supported in app/'
)
await cleanup ( )
} )
2024-01-16 12:31:28 +01:00
it ( 'should throw an error when metadata export is used in client components' , async ( ) = > {
2023-06-21 21:47:21 +02:00
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/client-with-errors/metadata-export'
)
const pageFile = 'app/client-with-errors/metadata-export/page.js'
const content = await next . readFile ( pageFile )
// Add `metadata` error
let uncomment = content . replace (
'// export const metadata' ,
'export const metadata'
)
await session . patch ( pageFile , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
'You are attempting to export "metadata" from a component marked with "use client", which is disallowed.'
)
// Restore file
await session . patch ( pageFile , content )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( false )
2023-06-21 21:47:21 +02:00
// Add `generateMetadata` error
uncomment = content . replace (
'// export async function generateMetadata' ,
'export async function generateMetadata'
)
await session . patch ( pageFile , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
'You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed.'
)
2024-01-16 12:31:28 +01:00
// Fix the error again to test error overlay works with hmr rebuild
await session . patch ( pageFile , content )
expect ( await session . hasRedbox ( ) ) . toBe ( false )
2023-06-21 21:47:21 +02:00
await cleanup ( )
} )
it ( 'should throw an error when metadata exports are used together in server components' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/metadata-export'
)
const pageFile = 'app/server-with-errors/metadata-export/page.js'
const content = await next . readFile ( pageFile )
const uncomment = content . replace (
'// export async function generateMetadata' ,
'export async function generateMetadata'
)
await session . patch ( pageFile , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
'"metadata" and "generateMetadata" cannot be exported at the same time, please keep one of them.'
)
await cleanup ( )
} )
// TODO: investigate flakey test case
it . skip ( 'should throw an error when getStaticProps is used' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/client-with-errors/get-static-props'
)
const pageFile = 'app/client-with-errors/get-static-props/page.js'
const content = await next . readFile ( pageFile )
const uncomment = content . replace (
'// export function getStaticProps' ,
'export function getStaticProps'
)
await session . patch ( pageFile , uncomment )
await next . patchFile ( pageFile , content )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
'"getStaticProps" is not supported in app/'
)
await cleanup ( )
} )
it ( 'should error when page component export is not valid' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/page-export'
)
await next . patchFile (
'app/server-with-errors/page-export/page.js' ,
'export const a = 123'
)
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxDescription ( ) ) . toInclude (
2023-07-20 02:32:57 +02:00
'The default export is not a React Component in page: "/server-with-errors/page-export"'
)
await cleanup ( )
} )
it ( 'should error when page component export is not valid on initial load' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/page-export-initial-error'
)
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-07-20 02:32:57 +02:00
expect ( await session . getRedboxDescription ( ) ) . toInclude (
'The default export is not a React Component in page: "/server-with-errors/page-export-initial-error"'
2023-06-21 21:47:21 +02:00
)
await cleanup ( )
} )
it ( 'should throw an error when "use client" is on the top level but after other expressions' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/swc/use-client'
)
const pageFile = 'app/swc/use-client/page.js'
const content = await next . readFile ( pageFile )
const uncomment = content . replace ( "// 'use client'" , "'use client'" )
await next . patchFile ( pageFile , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
'directive must be placed before other expressions'
)
await cleanup ( )
} )
it ( 'should throw an error when "Component" is imported in server components' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/class-component'
)
const pageFile = 'app/server-with-errors/class-component/page.js'
const content = await next . readFile ( pageFile )
const uncomment = content . replace (
"// import { Component } from 'react'" ,
"import { Component } from 'react'"
)
await session . patch ( pageFile , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
` You’ re importing a class component. It only works in a Client Component `
)
await cleanup ( )
} )
it ( 'should allow to use and handle rsc poisoning client-only' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/client-only-in-server'
)
const file =
'app/server-with-errors/client-only-in-server/client-only-lib.js'
const content = await next . readFile ( file )
const uncomment = content . replace (
"// import 'client-only'" ,
"import 'client-only'"
)
await next . patchFile ( file , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2024-02-16 01:52:29 +01:00
if ( isTurbopack ) {
2024-02-27 20:07:32 +01:00
// TODO: fix the issue ordering.
// turbopack emits the resolve issue first instead of the transform issue.
2024-02-16 01:52:29 +01:00
expect ( await session . getRedboxSource ( ) ) . toMatchInlineSnapshot ( `
" . / app / server - with - errors / client - only - in - server / client - only - lib.js :1 : 1
2024-02-26 11:19:31 +01:00
Ecmascript file had an error
2024-02-16 01:52:29 +01:00
> 1 | import 'client-only'
| ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
2 |
3 | export default function ClientOnlyLib() {
4 | return 'client-only-lib'
You 're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client", so they' re Server Components by default . \ \ nLearn more : https : //nextjs.org/docs/getting-started/react-essentials\\n\\n"
` )
} else {
expect ( await session . getRedboxSource ( ) ) . toInclude (
` You're importing a component that imports client-only. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default. `
)
}
2023-06-21 21:47:21 +02:00
await cleanup ( )
} )
2023-12-14 21:09:41 +01:00
const invalidReactServerApis = [
'Component' ,
'createContext' ,
'createFactory' ,
'PureComponent' ,
'useDeferredValue' ,
'useEffect' ,
'useImperativeHandle' ,
'useInsertionEffect' ,
'useLayoutEffect' ,
'useReducer' ,
'useRef' ,
'useState' ,
'useSyncExternalStore' ,
'useTransition' ,
'useOptimistic' ,
]
for ( const api of invalidReactServerApis ) {
it ( ` should error when ${ api } from react is used in server component ` , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
` /server-with-errors/react-apis/ ${ api . toLowerCase ( ) } `
)
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-12-14 21:09:41 +01:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
// `Component` has a custom error message
api === 'Component'
? ` You’ re importing a class component. It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default. `
: ` You're importing a component that needs ${ api } . It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components by default. `
)
await cleanup ( )
} )
}
2023-12-15 10:12:46 +01:00
const invalidReactDomServerApis = [
'findDOMNode' ,
'flushSync' ,
'unstable_batchedUpdates' ,
'useFormStatus' ,
'useFormState' ,
]
for ( const api of invalidReactDomServerApis ) {
it ( ` should error when ${ api } from react-dom is used in server component ` , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
` /server-with-errors/react-dom-apis/ ${ api . toLowerCase ( ) } `
)
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-12-15 10:12:46 +01:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
2024-01-05 16:14:06 +01:00
` You're importing a component that needs ${ api } . It only works in a Client Component but none of its parents are marked with "use client", so they're Server Components `
2023-12-15 10:12:46 +01:00
)
await cleanup ( )
} )
}
2023-06-21 21:47:21 +02:00
it ( 'should allow to use and handle rsc poisoning server-only' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/client-with-errors/server-only-in-client'
)
const file =
'app/client-with-errors/server-only-in-client/server-only-lib.js'
const content = await next . readFile ( file )
const uncomment = content . replace (
"// import 'server-only'" ,
"import 'server-only'"
)
await session . patch ( file , uncomment )
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxSource ( ) ) . toInclude (
` You're importing a component that needs server-only. That only works in a Server Component but one of its parents is marked with "use client", so it's a Client Component. `
)
await cleanup ( )
} )
it ( 'should error for invalid undefined module retuning from next dynamic' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/client-with-errors/dynamic'
)
const file = 'app/client-with-errors/dynamic/page.js'
const content = await next . readFile ( file )
await session . patch (
file ,
content . replace ( '() => <p>hello dynamic world</p>' , 'undefined' )
)
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
expect ( await session . getRedboxDescription ( ) ) . toInclude (
` Element type is invalid. Received a promise that resolves to: undefined. Lazy element type must resolve to a class or function. `
)
await cleanup ( )
} )
it ( 'should throw an error when error file is a server component' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/error-file'
)
// Remove "use client"
await session . patch (
'app/server-with-errors/error-file/error.js' ,
'export default function Error() {}'
)
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2024-01-05 16:14:06 +01:00
await check (
( ) = > session . getRedboxSource ( ) ,
/must be a Client \n| Component/
)
2024-02-06 18:15:07 +01:00
if ( process . env . TURBOPACK ) {
expect ( next . normalizeTestDirContent ( await session . getRedboxSource ( ) ) )
. toMatchInlineSnapshot ( `
2024-02-06 23:24:30 +01:00
" . / app / server - with - errors / error - file / error.js :1 : 1
2024-02-26 11:19:31 +01:00
Ecmascript file had an error
2024-02-06 18:15:07 +01:00
> 1 | export default function Error ( ) { }
| ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
app / server - with - errors / error - file / error . js must be a Client Component . Add the "use client" directive the top of the file to resolve this issue .
Learn more : https : //nextjs.org/docs/getting-started/react-essentials#client-components"
` )
} else {
expect ( next . normalizeTestDirContent ( await session . getRedboxSource ( ) ) )
. toMatchInlineSnapshot ( `
" . / app / server - with - errors / error - file / error . js
Error :
x TEST_DIR / app / server - with - errors / error - file / error . js must be a Client Component . Add the "use client" directive the top
| of the file to resolve this issue .
| Learn more : https : //nextjs.org/docs/getting-started/react-essentials#client-components
|
|
, - [ TEST_DIR / app / server - with - errors / error - file / error.js :1 : 1 ]
1 | export default function Error ( ) { }
: ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
\ ` ----
Import trace for requested module :
. / app / server - with - errors / error - file / error . js "
` )
}
2023-01-25 00:13:37 +01:00
2023-06-21 21:47:21 +02:00
await cleanup ( )
} )
2023-02-14 22:49:35 +01:00
2023-06-21 21:47:21 +02:00
it ( 'should throw an error when error file is a server component with empty error file' , async ( ) = > {
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/server-with-errors/error-file'
)
2023-01-25 00:13:37 +01:00
2023-06-21 21:47:21 +02:00
// Empty file
await session . patch ( 'app/server-with-errors/error-file/error.js' , '' )
2023-01-25 00:13:37 +01:00
2024-01-15 09:36:44 +01:00
expect ( await session . hasRedbox ( ) ) . toBe ( true )
2023-06-21 21:47:21 +02:00
await check ( ( ) = > session . getRedboxSource ( ) , /must be a Client Component/ )
2023-01-25 00:13:37 +01:00
2023-06-21 21:47:21 +02:00
// TODO: investigate flakey snapshot due to spacing below
// expect(next.normalizeTestDirContent(await session.getRedboxSource()))
2024-01-16 11:11:06 +01:00
// .toMatchInlineSnapshot(n`
2023-06-21 21:47:21 +02:00
// "./app/server-with-errors/error-file/error.js
// ReactServerComponentsError:
2023-01-25 00:13:37 +01:00
2023-10-19 19:38:24 +02:00
// ./app/server-with-errors/error-file/error.js must be a Client Component. Add the "use client" directive the top of the file to resolve this issue.
2023-01-25 00:13:37 +01:00
2023-06-21 21:47:21 +02:00
// ,-[TEST_DIR/app/server-with-errors/error-file/error.js:1:1]
// 1 |
// : ^
// \`----
2023-05-28 06:02:31 +02:00
2023-06-21 21:47:21 +02:00
// Import path:
// ./app/server-with-errors/error-file/error.js"
2024-01-16 11:11:06 +01:00
// `)
2023-01-25 00:13:37 +01:00
2023-06-21 21:47:21 +02:00
await cleanup ( )
} )
2023-02-07 20:56:03 +01:00
2023-06-21 21:47:21 +02:00
it ( 'should freeze parent resolved metadata to avoid mutating in generateMetadata' , async ( ) = > {
const pagePath = 'app/metadata/mutate/page.js'
const content = outdent `
export default function page ( props ) {
2023-03-02 00:09:16 +01:00
return < p > mutate < / p >
}
export async function generateMetadata ( props , parent ) {
const parentMetadata = await parent
parentMetadata . x = 1
return {
. . . parentMetadata ,
}
2023-06-21 21:47:21 +02:00
}
`
const { session , cleanup } = await sandbox (
next ,
undefined ,
'/metadata/mutate'
)
await session . patch ( pagePath , content )
await check (
2024-01-15 09:36:44 +01:00
async ( ) = > ( ( await session . hasRedbox ( ) ) ? 'success' : 'fail' ) ,
2023-06-21 21:47:21 +02:00
/success/
)
expect ( await session . getRedboxDescription ( ) ) . toContain (
'Cannot add property x, object is not extensible'
)
await cleanup ( )
} )
} )