diff --git a/package.json b/package.json index 62d7fdd69afb..974d2c2c4e70 100644 --- a/package.json +++ b/package.json @@ -169,6 +169,7 @@ "npm-run-all": "4.1.5", "nprogress": "0.2.0", "open": "9.0.0", + "outdent": "0.8.0", "pixrem": "5.0.0", "playwright-chromium": "1.28.1", "plop": "3.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1723a51cd18..d538c857cf38 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,6 +128,7 @@ importers: npm-run-all: 4.1.5 nprogress: 0.2.0 open: 9.0.0 + outdent: 0.8.0 pixrem: 5.0.0 playwright-chromium: 1.28.1 plop: 3.0.5 @@ -295,6 +296,7 @@ importers: npm-run-all: 4.1.5 nprogress: 0.2.0 open: 9.0.0 + outdent: 0.8.0 pixrem: 5.0.0 playwright-chromium: 1.28.1 plop: 3.0.5 @@ -18878,6 +18880,10 @@ packages: os-tmpdir: 1.0.2 dev: true + /outdent/0.8.0: + resolution: {integrity: sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A==} + dev: true + /p-cancelable/0.3.0: resolution: {integrity: sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==} engines: {node: '>=4'} diff --git a/test/development/acceptance-app/ReactRefresh.test.ts b/test/development/acceptance-app/ReactRefresh.test.ts index fafd38c09b6b..cad69548c4f6 100644 --- a/test/development/acceptance-app/ReactRefresh.test.ts +++ b/test/development/acceptance-app/ReactRefresh.test.ts @@ -1,29 +1,24 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' describe('ReactRefresh app', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, }) - afterAll(() => next.destroy()) test('can edit a component without losing state', async () => { const { session, cleanup } = await sandbox(next) await session.patch( 'index.js', - ` + outdent` import { useCallback, useState } from 'react' export default function Index() { const [count, setCount] = useState(0) @@ -43,7 +38,7 @@ describe('ReactRefresh app', () => { ).toBe('1') await session.patch( 'index.js', - ` + outdent` import { useCallback, useState } from 'react' export default function Index() { const [count, setCount] = useState(0) @@ -72,7 +67,7 @@ describe('ReactRefresh app', () => { await session.write( 'NudgeOverview.js', - ` + outdent` import * as React from 'react'; import { foo } from './routes'; @@ -88,7 +83,7 @@ describe('ReactRefresh app', () => { await session.write( 'SurveyOverview.js', - ` + outdent` const SurveyOverview = () => { return 100; }; @@ -99,7 +94,7 @@ describe('ReactRefresh app', () => { await session.write( 'Milestones.js', - ` + outdent` import React from 'react'; import { fragment } from './DashboardPage'; @@ -115,7 +110,7 @@ describe('ReactRefresh app', () => { await session.write( 'DashboardPage.js', - ` + outdent` import React from 'react'; import Milestones from './Milestones'; @@ -140,7 +135,7 @@ describe('ReactRefresh app', () => { await session.write( 'routes.js', - ` + outdent` import DashboardPage from './DashboardPage'; export const foo = {}; @@ -154,7 +149,7 @@ describe('ReactRefresh app', () => { await session.patch( 'index.js', - ` + outdent` import * as React from 'react'; import DashboardPage from './routes'; @@ -173,7 +168,7 @@ describe('ReactRefresh app', () => { let didFullRefresh = !(await session.patch( 'SurveyOverview.js', - ` + outdent` const SurveyOverview = () => { return 200; }; @@ -189,7 +184,7 @@ describe('ReactRefresh app', () => { didFullRefresh = !(await session.patch( 'index.js', - ` + outdent` import * as React from 'react'; import DashboardPage from './routes'; @@ -209,7 +204,7 @@ describe('ReactRefresh app', () => { didFullRefresh = !(await session.patch( 'SurveyOverview.js', - ` + outdent` const SurveyOverview = () => { return 300; }; diff --git a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts index f2f952c68256..2bfdd40e93c3 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox-builtins.test.ts @@ -1,151 +1,146 @@ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' -import { getSnapshotTestDescribe } from 'next-test-utils' +import { describeVariants as describe } from 'next-test-utils' +import { outdent } from 'outdent' // TODO-APP: Investigate snapshot mismatch -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`ReactRefreshLogBox app ${variant}`, () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef( - path.join(__dirname, 'fixtures', 'default-template') - ), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) - }) - afterAll(() => next.destroy()) - - // Module trace is only available with webpack 5 - test('Node.js builtins', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const dns = require('dns') - module.exports = dns - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - ]) - ) - - await session.patch( - 'index.js', - ` - import pkg from 'my-package' - - export default function Hello() { - return (pkg ?

Package loaded

:

Package did not load

) - } - ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() - - await cleanup() - }) - - test('Module not found', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - `import Comp from 'b' - export default function Oops() { - return ( -
- lol -
- ) - } - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - - const source = await session.getRedboxSource() - expect(source).toMatchSnapshot() - - await cleanup() - }) - - test('Module not found empty import trace', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'app/page.js', - `'use client' - import Comp from 'b' - export default function Oops() { - return ( -
- lol -
- ) - } - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - - const source = await session.getRedboxSource() - expect(source).toMatchSnapshot() - - await cleanup() - }) - - test('Module not found missing global CSS', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - `'use client' - import './non-existent.css' +describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, + }) + + // Module trace is only available with webpack 5 + test('Node.js builtins', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/my-package/index.js', + outdent` + const dns = require('dns') + module.exports = dns + `, + ], + [ + 'node_modules/my-package/package.json', + outdent` + { + "name": "my-package", + "version": "0.0.1" + } + `, + ], + ]) + ) + + await session.patch( + 'index.js', + outdent` + import pkg from 'my-package' + + export default function Hello() { + return (pkg ?

Package loaded

:

Package did not load

) + } + ` + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() + + await cleanup() + }) + + test('Module not found', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` + import Comp from 'b' + export default function Oops() { + return ( +
+ lol +
+ ) + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source).toMatchSnapshot() + + await cleanup() + }) + + test('Module not found empty import trace', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'app/page.js', + outdent` + 'use client' + import Comp from 'b' + export default function Oops() { + return ( +
+ lol +
+ ) + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source).toMatchSnapshot() + + await cleanup() + }) + + test('Module not found missing global CSS', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + import './non-existent.css' + export default function Page(props) { + return

index page

+ } + `, + ], + ]) + ) + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source).toMatchSnapshot() + + await session.patch( + 'app/page.js', + outdent` + 'use client' export default function Page(props) { return

index page

} - `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - - const source = await session.getRedboxSource() - expect(source).toMatchSnapshot() - - await session.patch( - 'app/page.js', - `'use client' - export default function Page(props) { - return

index page

- } - ` - ) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.documentElement.innerHTML) - ).toContain('index page') - - await cleanup() - }) + ` + ) + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.documentElement.innerHTML) + ).toContain('index page') + + await cleanup() }) -} +}) diff --git a/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts b/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts index 2524348db8e8..b2ce9f861b7d 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox-scss.test.ts @@ -1,24 +1,19 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' describe('ReactRefreshLogBox app', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - skipStart: true, - dependencies: { - sass: 'latest', - react: 'latest', - 'react-dom': 'latest', - }, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + sass: 'latest', + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, }) - afterAll(() => next.destroy()) test('scss syntax errors', async () => { const { session, cleanup } = await sandbox(next) @@ -26,7 +21,7 @@ describe('ReactRefreshLogBox app', () => { await session.write('index.module.scss', `.button { font-size: 5px; }`) await session.patch( 'index.js', - ` + outdent` import './index.module.scss'; export default () => { return ( diff --git a/test/development/acceptance-app/ReactRefreshLogBox.test.ts b/test/development/acceptance-app/ReactRefreshLogBox.test.ts index f6f7e6b51d02..ba71baa42f42 100644 --- a/test/development/acceptance-app/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBox.test.ts @@ -1,34 +1,26 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { check, getSnapshotTestDescribe } from 'next-test-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { check, describeVariants as describe } from 'next-test-utils' import path from 'path' +import { outdent } from 'outdent' + +describe.each(['default', 'turbo'])('ReactRefreshLogBox app %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, + }) -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`ReactRefreshLogBox app ${variant}`, () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef( - path.join(__dirname, 'fixtures', 'default-template') - ), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) - }) - afterAll(() => next.destroy()) - - test('should strip whitespace correctly with newline', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` + test('should strip whitespace correctly with newline', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` export default function Page() { return ( <> @@ -44,24 +36,24 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - await session.evaluate(() => document.querySelector('a').click()) + ) + await session.evaluate(() => document.querySelector('a').click()) - await session.waitForAndOpenRuntimeError() - expect(await session.getRedboxSource()).toMatchSnapshot() + await session.waitForAndOpenRuntimeError() + expect(await session.getRedboxSource()).toMatchSnapshot() - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807 - test('module init error not shown', async () => { - // Start here: - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807 + test('module init error not shown', async () => { + // Start here: + const { session, cleanup } = await sandbox(next) - // We start here. - await session.patch( - 'index.js', - ` + // We start here. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { render() { @@ -70,16 +62,16 @@ for (const variant of ['default', 'turbo']) { } export default ClassDefault; ` - ) + ) - expect( - await session.evaluate(() => document.querySelector('h1').textContent) - ).toBe('Default Export') + expect( + await session.evaluate(() => document.querySelector('h1').textContent) + ).toBe('Default Export') - // Add a throw in module init phase: - await session.patch( - 'index.js', - ` + // Add a throw in module init phase: + await session.patch( + 'index.js', + outdent` // top offset for snapshot import * as React from 'react'; throw new Error('no') @@ -90,33 +82,33 @@ for (const variant of ['default', 'turbo']) { } export default ClassDefault; ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - if (process.platform === 'win32') { - expect(await session.getRedboxSource()).toMatchSnapshot() - } else { - expect(await session.getRedboxSource()).toMatchSnapshot() - } + expect(await session.hasRedbox(true)).toBe(true) + if (process.platform === 'win32') { + expect(await session.getRedboxSource()).toMatchSnapshot() + } else { + expect(await session.getRedboxSource()).toMatchSnapshot() + } - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127 - test('boundaries', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127 + test('boundaries', async () => { + const { session, cleanup } = await sandbox(next) - await session.write( - 'FunctionDefault.js', - ` + await session.write( + 'FunctionDefault.js', + outdent` export default function FunctionDefault() { return

hello

} ` - ) - await session.patch( - 'index.js', - ` + ) + await session.patch( + 'index.js', + outdent` import FunctionDefault from './FunctionDefault.js' import * as React from 'react' class ErrorBoundary extends React.Component { @@ -146,58 +138,59 @@ for (const variant of ['default', 'turbo']) { } export default App; ` - ) + ) - expect( - await session.evaluate(() => document.querySelector('h2').textContent) - ).toBe('hello') + expect( + await session.evaluate(() => document.querySelector('h2').textContent) + ).toBe('hello') - await session.write( - 'FunctionDefault.js', - `export default function FunctionDefault() { throw new Error('no'); }` - ) + await session.write( + 'FunctionDefault.js', + `export default function FunctionDefault() { throw new Error('no'); }` + ) - await session.waitForAndOpenRuntimeError() - expect(await session.getRedboxSource()).toMatchSnapshot() - expect( - await session.evaluate(() => document.querySelector('h2').textContent) - ).toBe('error') + await session.waitForAndOpenRuntimeError() + expect(await session.getRedboxSource()).toMatchSnapshot() + expect( + await session.evaluate(() => document.querySelector('h2').textContent) + ).toBe('error') - await cleanup() - }) + await cleanup() + }) - // TODO: investigate why this fails when running outside of the Next.js - // monorepo e.g. fails when using yarn create next-app - // https://github.com/vercel/next.js/pull/23203 - test.skip('internal package errors', async () => { - const { session, cleanup } = await sandbox(next) + // TODO: investigate why this fails when running outside of the Next.js + // monorepo e.g. fails when using yarn create next-app + // https://github.com/vercel/next.js/pull/23203 + test.skip('internal package errors', async () => { + const { session, cleanup } = await sandbox(next) - // Make a react build-time error. - await session.patch( - 'index.js', - ` + // Make a react build-time error. + await session.patch( + 'index.js', + outdent` export default function FunctionNamed() { return
{{}}
- }` - ) + } + ` + ) - expect(await session.hasRedbox(true)).toBe(true) - // We internally only check the script path, not including the line number - // and error message because the error comes from an external library. - // This test ensures that the errored script path is correctly resolved. - expect(await session.getRedboxSource()).toContain( - `../../../../packages/next/dist/pages/_document.js` - ) + expect(await session.hasRedbox(true)).toBe(true) + // We internally only check the script path, not including the line number + // and error message because the error comes from an external library. + // This test ensures that the errored script path is correctly resolved. + expect(await session.getRedboxSource()).toContain( + `../../../../packages/next/dist/pages/_document.js` + ) - await cleanup() - }) + await cleanup() + }) - test('unterminated JSX', async () => { - const { session, cleanup } = await sandbox(next) + test('unterminated JSX', async () => { + const { session, cleanup } = await sandbox(next) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` export default () => { return (
@@ -206,13 +199,13 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` export default () => { return (
@@ -221,31 +214,30 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) + expect(await session.hasRedbox(true)).toBe(true) - const source = await session.getRedboxSource() - expect(next.normalizeTestDirContent(source)).toMatchInlineSnapshot( - next.normalizeSnapshot(` + const source = await session.getRedboxSource() + expect(next.normalizeTestDirContent(source)).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./index.js Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? - ,-[TEST_DIR/index.js:5:1] - 5 |

lol

- 6 | div - 7 | ) - 8 | } - : ^ - 9 | + ,-[TEST_DIR/index.js:4:1] + 4 |

lol

+ 5 | div + 6 | ) + 7 | } + : ^ \`---- x Unexpected eof - ,-[TEST_DIR/index.js:6:1] - 6 | div - 7 | ) - 8 | } - 9 | + ,-[TEST_DIR/index.js:4:1] + 4 |

lol

+ 5 | div + 6 | ) + 7 | } \`---- Caused by: @@ -255,27 +247,27 @@ for (const variant of ['default', 'turbo']) { ./index.js ./app/page.js" `) - ) + ) - await cleanup() - }) + await cleanup() + }) - // Module trace is only available with webpack 5 - test('conversion to class component (1)', async () => { - const { session, cleanup } = await sandbox(next) + // Module trace is only available with webpack 5 + test('conversion to class component (1)', async () => { + const { session, cleanup } = await sandbox(next) - await session.write( - 'Child.js', - ` + await session.write( + 'Child.js', + outdent` export default function ClickCount() { return

hello

} ` - ) + ) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import Child from './Child'; export default function Home() { @@ -286,16 +278,16 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('hello') + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('hello') - await session.patch( - 'Child.js', - ` + await session.patch( + 'Child.js', + outdent` import { Component } from 'react'; export default class ClickCount extends Component { render() { @@ -303,38 +295,38 @@ for (const variant of ['default', 'turbo']) { } } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() - await session.patch( - 'Child.js', - ` - import { Component } from 'react'; + await session.patch( + 'Child.js', + outdent` + import { Component } from 'react'; export default class ClickCount extends Component { render() { return

hello new

} } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('hello new') + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('hello new') - await cleanup() - }) + await cleanup() + }) - test('css syntax errors', async () => { - const { session, cleanup } = await sandbox(next) + test('css syntax errors', async () => { + const { session, cleanup } = await sandbox(next) - await session.write('index.module.css', `.button {}`) - await session.patch( - 'index.js', - ` + await session.write('index.module.css', `.button {}`) + await session.patch( + 'index.js', + outdent` import './index.module.css'; export default () => { return ( @@ -344,35 +336,35 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - expect(await session.hasRedbox(false)).toBe(false) - - // Syntax error - await session.patch('index.module.css', `.button {`) - expect(await session.hasRedbox(true)).toBe(true) - const source = await session.getRedboxSource() - expect(source).toMatch('./index.module.css (1:1)') - expect(source).toMatch('Syntax error: ') - expect(source).toMatch('Unclosed block') - expect(source).toMatch('> 1 | .button {') - expect(source).toMatch(' | ^') - - // Not local error - await session.patch('index.module.css', `button {}`) - expect(await session.hasRedbox(true)).toBe(true) - const source2 = await session.getRedboxSource() - expect(source2).toMatchSnapshot() + ) - await cleanup() - }) + expect(await session.hasRedbox(false)).toBe(false) + + // Syntax error + await session.patch('index.module.css', `.button {`) + expect(await session.hasRedbox(true)).toBe(true) + const source = await session.getRedboxSource() + expect(source).toMatch('./index.module.css (1:1)') + expect(source).toMatch('Syntax error: ') + expect(source).toMatch('Unclosed block') + expect(source).toMatch('> 1 | .button {') + expect(source).toMatch(' | ^') + + // Not local error + await session.patch('index.module.css', `button {}`) + expect(await session.hasRedbox(true)).toBe(true) + const source2 = await session.getRedboxSource() + expect(source2).toMatchSnapshot() + + await cleanup() + }) - test('logbox: anchors links in error messages', async () => { - const { session, cleanup } = await sandbox(next) + test('logbox: anchors links in error messages', async () => { + const { session, cleanup } = await sandbox(next) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -386,38 +378,38 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - await session.evaluate(() => document.querySelector('button').click()) - await session.waitForAndOpenRuntimeError() + ) - const header = await session.getRedboxDescription() - expect(header).toMatchSnapshot() - expect( - await session.evaluate( - () => + await session.evaluate(() => document.querySelector('button').click()) + await session.waitForAndOpenRuntimeError() + + const header = await session.getRedboxDescription() + expect(header).toMatchSnapshot() + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(1) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(1) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -431,38 +423,38 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - await session.evaluate(() => document.querySelector('button').click()) - await session.waitForAndOpenRuntimeError() + ) - const header2 = await session.getRedboxDescription() - expect(header2).toMatchSnapshot() - expect( - await session.evaluate( - () => + await session.evaluate(() => document.querySelector('button').click()) + await session.waitForAndOpenRuntimeError() + + const header2 = await session.getRedboxDescription() + expect(header2).toMatchSnapshot() + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(1) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(1) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -476,38 +468,38 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - await session.evaluate(() => document.querySelector('button').click()) - await session.waitForAndOpenRuntimeError() + ) - const header3 = await session.getRedboxDescription() - expect(header3).toMatchSnapshot() - expect( - await session.evaluate( - () => + await session.evaluate(() => document.querySelector('button').click()) + await session.waitForAndOpenRuntimeError() + + const header3 = await session.getRedboxDescription() + expect(header3).toMatchSnapshot() + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(1) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(1) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -521,59 +513,59 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - await session.evaluate(() => document.querySelector('button').click()) - await session.waitForAndOpenRuntimeError() + await session.evaluate(() => document.querySelector('button').click()) + await session.waitForAndOpenRuntimeError() - const header4 = await session.getRedboxDescription() - expect(header4).toMatchInlineSnapshot( - `"Error: multiple http://nextjs.org links http://example.com"` - ) - expect( - await session.evaluate( - () => + const header4 = await session.getRedboxDescription() + expect(header4).toMatchInlineSnapshot( + `"Error: multiple http://nextjs.org links http://example.com"` + ) + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(2) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(2) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(2)' - ) as any - ).href - ) - ).toMatchSnapshot() + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() + expect( + await session.evaluate( + () => + ( + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(2)' + ) as any + ).href + ) + ).toMatchSnapshot() - await cleanup() - }) + await cleanup() + }) - // TODO-APP: Catch errors that happen before useEffect - test.skip('non-Error errors are handled properly', async () => { - const { session, cleanup } = await sandbox(next) + // TODO-APP: Catch errors that happen before useEffect + test.skip('non-Error errors are handled properly', async () => { + const { session, cleanup } = await sandbox(next) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` export default () => { throw {'a': 1, 'b': 'x'}; return ( @@ -581,28 +573,28 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot( - `"Error: {\\"a\\":1,\\"b\\":\\"x\\"}"` - ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toMatchInlineSnapshot( + `"Error: {\\"a\\":1,\\"b\\":\\"x\\"}"` + ) - // fix previous error - await session.patch( - 'index.js', - ` + // fix previous error + await session.patch( + 'index.js', + outdent` export default () => { return (
hello
) } ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await session.patch( + 'index.js', + outdent` class Hello {} export default () => { @@ -612,27 +604,27 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toContain( - `Error: class Hello {` - ) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toContain( + `Error: class Hello {` + ) - // fix previous error - await session.patch( - 'index.js', - ` + // fix previous error + await session.patch( + 'index.js', + outdent` export default () => { return (
hello
) } ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await session.patch( + 'index.js', + outdent` export default () => { throw "string error" return ( @@ -640,27 +632,27 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot( - `"Error: string error"` - ) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toMatchInlineSnapshot( + `"Error: string error"` + ) - // fix previous error - await session.patch( - 'index.js', - ` + // fix previous error + await session.patch( + 'index.js', + outdent` export default () => { return (
hello
) } ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await session.patch( + 'index.js', + outdent` export default () => { throw null return ( @@ -668,218 +660,217 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toContain( - `Error: A null error was thrown` - ) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toContain( + `Error: A null error was thrown` + ) - await cleanup() - }) + await cleanup() + }) - test('Should not show __webpack_exports__ when exporting anonymous arrow function', async () => { - const { session, cleanup } = await sandbox(next) + test('Should not show __webpack_exports__ when exporting anonymous arrow function', async () => { + const { session, cleanup } = await sandbox(next) - await session.patch( - 'index.js', - ` - export default () => { - if (typeof window !== 'undefined') { - throw new Error('test') - } - - return null - } + await session.patch( + 'index.js', + outdent` + export default () => { + if (typeof window !== 'undefined') { + throw new Error('test') + } + return null + } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() - await cleanup() - }) - - test('Unhandled errors and rejections opens up in the minimized state', async () => { - const { session, browser, cleanup } = await sandbox(next) - - const file = ` - export default function Index() { - // - setTimeout(() => { - throw new Error('Unhandled error') - }, 0) - setTimeout(() => { - Promise.reject(new Error('Undhandled rejection')) - }, 0) - return ( - <> - - - - ) - } + await cleanup() + }) + + test('Unhandled errors and rejections opens up in the minimized state', async () => { + const { session, browser, cleanup } = await sandbox(next) + + const file = outdent` + export default function Index() { + // + setTimeout(() => { + throw new Error('Unhandled error') + }, 0) + setTimeout(() => { + Promise.reject(new Error('Undhandled rejection')) + }, 0) + return ( + <> + + + + ) + } ` - await session.patch('index.js', file) + await session.patch('index.js', file) - // Unhandled error and rejection in setTimeout - expect( - await browser.waitForElementByCss('.nextjs-toast-errors').text() - ).toBe('2 errors') + // Unhandled error and rejection in setTimeout + expect( + await browser.waitForElementByCss('.nextjs-toast-errors').text() + ).toBe('2 errors') - // Unhandled error in event handler - await browser.elementById('unhandled-error').click() - await check( - () => browser.elementByCss('.nextjs-toast-errors').text(), - /3 errors/ - ) + // Unhandled error in event handler + await browser.elementById('unhandled-error').click() + await check( + () => browser.elementByCss('.nextjs-toast-errors').text(), + /3 errors/ + ) - // Unhandled rejection in event handler - await browser.elementById('unhandled-rejection').click() - await check( - () => browser.elementByCss('.nextjs-toast-errors').text(), - /4 errors/ - ) - expect(await session.hasRedbox(false)).toBe(false) + // Unhandled rejection in event handler + await browser.elementById('unhandled-rejection').click() + await check( + () => browser.elementByCss('.nextjs-toast-errors').text(), + /4 errors/ + ) + expect(await session.hasRedbox(false)).toBe(false) - // Add Component error - await session.patch( - 'index.js', - file.replace( - '//', - "if (typeof window !== 'undefined') throw new Error('Component error')" - ) + // Add Component error + await session.patch( + 'index.js', + file.replace( + '//', + "if (typeof window !== 'undefined') throw new Error('Component error')" ) + ) - // Render error should "win" and show up in fullscreen - expect(await session.hasRedbox(true)).toBe(true) + // Render error should "win" and show up in fullscreen + expect(await session.hasRedbox(true)).toBe(true) - await cleanup() - }) - - test.each([['server'], ['client']])( - 'Call stack count is correct for %s error', - async (pageType: string) => { - const fixture = - pageType === 'server' - ? new Map([ - [ - 'app/page.js', - ` - export default function Page() { - throw new Error('Server error') - } -`, - ], - ]) - : new Map([ - [ - 'app/page.js', - ` - 'use client' - export default function Page() { - if (typeof window !== 'undefined') { - throw new Error('Client error') - } - return null - } -`, - ], - ]) - - const { session, browser, cleanup } = await sandbox(next, fixture) + await cleanup() + }) - const getCallStackCount = async () => - (await browser.elementsByCss('[data-nextjs-call-stack-frame]')).length + test.each(['server', 'client'])( + 'Call stack count is correct for %s error', + async (pageType) => { + const fixture = + pageType === 'server' + ? new Map([ + [ + 'app/page.js', + outdent` + export default function Page() { + throw new Error('Server error') + } + `, + ], + ]) + : new Map([ + [ + 'app/page.js', + outdent` + 'use client' + export default function Page() { + if (typeof window !== 'undefined') { + throw new Error('Client error') + } + return null + } + `, + ], + ]) + + const { session, browser, cleanup } = await sandbox(next, fixture) + + const getCallStackCount = async () => + (await browser.elementsByCss('[data-nextjs-call-stack-frame]')).length - expect(await session.hasRedbox(true)).toBe(true) + expect(await session.hasRedbox(true)).toBe(true) - // Open full Call Stack - await browser - .elementByCss('[data-nextjs-data-runtime-error-collapsed-action]') - .click() + // Open full Call Stack + await browser + .elementByCss('[data-nextjs-data-runtime-error-collapsed-action]') + .click() - // Expect more than the default amount of frames - // The default stackTraceLimit results in max 9 [data-nextjs-call-stack-frame] elements - expect(await getCallStackCount()).toBeGreaterThan(9) + // Expect more than the default amount of frames + // The default stackTraceLimit results in max 9 [data-nextjs-call-stack-frame] elements + expect(await getCallStackCount()).toBeGreaterThan(9) - await cleanup() - } + await cleanup() + } + ) + + test('Server component errors should open up in fullscreen', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + // Start with error + [ + 'app/page.js', + outdent` + export default function Page() { + throw new Error('Server component error') + return

Hello world

+ } + `, + ], + ]) ) + expect(await session.hasRedbox(true)).toBe(true) - test('Server component errors should open up in fullscreen', async () => { - const { session, browser, cleanup } = await sandbox( - next, - new Map([ - // Start with error - [ - 'app/page.js', - ` + // Remove error + await session.patch( + 'app/page.js', + outdent` export default function Page() { - throw new Error('Server component error') return

Hello world

} - `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - - // Remove error - await session.patch( - 'app/page.js', - ` - export default function Page() { - return

Hello world

- } ` - ) - expect(await browser.waitForElementByCss('#text').text()).toBe( - 'Hello world' - ) - expect(await session.hasRedbox(false)).toBe(false) + ) + expect(await browser.waitForElementByCss('#text').text()).toBe( + 'Hello world' + ) + expect(await session.hasRedbox(false)).toBe(false) - // Re-add error - await session.patch( - 'app/page.js', - ` - export default function Page() { - throw new Error('Server component error!') - return

Hello world

- } + // Re-add error + await session.patch( + 'app/page.js', + outdent` + export default function Page() { + throw new Error('Server component error!') + return

Hello world

+ } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) + expect(await session.hasRedbox(true)).toBe(true) - await cleanup() - }) + await cleanup() + }) - test('Import trace when module not found in layout', async () => { - const { session, cleanup } = await sandbox( - next, + test('Import trace when module not found in layout', async () => { + const { session, cleanup } = await sandbox( + next, - new Map([['app/module.js', `import "non-existing-module"`]]) - ) + new Map([['app/module.js', `import "non-existing-module"`]]) + ) - await session.patch( - 'app/layout.js', - ` + await session.patch( + 'app/layout.js', + outdent` import "./module" export default function RootLayout({ children }) { @@ -890,28 +881,27 @@ for (const variant of ['default', 'turbo']) { ) } - - ` - ) + ` + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() - await cleanup() - }) - - test("Can't resolve @import in CSS file", async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - ['app/styles1.css', '@import "./styles2.css"'], - ['app/styles2.css', '@import "./boom.css"'], - ]) - ) + await cleanup() + }) + + test("Can't resolve @import in CSS file", async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + ['app/styles1.css', '@import "./styles2.css"'], + ['app/styles2.css', '@import "./boom.css"'], + ]) + ) - await session.patch( - 'app/layout.js', - ` + await session.patch( + 'app/layout.js', + outdent` import "./styles1.css" export default function RootLayout({ children }) { @@ -922,31 +912,29 @@ for (const variant of ['default', 'turbo']) { ) } - - ` - ) + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() + + await cleanup() + }) + test.each([['server'], ['client']])( + '%s component can recover from error thrown in the module', + async (type: string) => { + const { session, cleanup } = await sandbox(next, undefined, '/' + type) + + await next.patchFile('index.js', "throw new Error('module error')") expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + await next.patchFile( + 'index.js', + 'export default function Page() {return

hello world

}' + ) + expect(await session.hasRedbox(false)).toBe(false) await cleanup() - }) - - test.each([['server'], ['client']])( - '%s component can recover from error thrown in the module', - async (type: string) => { - const { session, cleanup } = await sandbox(next, undefined, '/' + type) - - await next.patchFile('index.js', "throw new Error('module error')") - expect(await session.hasRedbox(true)).toBe(true) - await next.patchFile( - 'index.js', - 'export default function Page() {return

hello world

}' - ) - expect(await session.hasRedbox(false)).toBe(false) - - await cleanup() - } - ) - }) -} + } + ) +}) diff --git a/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts b/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts index 6e8b1deb48ff..144b487e6239 100644 --- a/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts +++ b/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts @@ -1,31 +1,26 @@ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' // TODO: re-enable these tests after figuring out what is causing // them to be so unreliable in CI describe.skip('ReactRefreshLogBox app', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, }) - afterAll(() => next.destroy()) test(' with multiple children', async () => { const { session, cleanup } = await sandbox(next) await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Index() { @@ -64,7 +59,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -80,7 +75,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -92,7 +87,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -116,7 +111,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -140,7 +135,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -164,7 +159,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -189,7 +184,7 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -220,7 +215,8 @@ describe.skip('ReactRefreshLogBox app', () => { await session.patch( 'app/page.js', - `'use client' + outdent` + 'use client' import myLibrary from 'my-non-existent-library' export async function getStaticProps() { return { diff --git a/test/development/acceptance-app/ReactRefreshModule.test.ts b/test/development/acceptance-app/ReactRefreshModule.test.ts index ba562fe1908e..59f629e2a924 100644 --- a/test/development/acceptance-app/ReactRefreshModule.test.ts +++ b/test/development/acceptance-app/ReactRefreshModule.test.ts @@ -1,22 +1,17 @@ -import { createNext, FileRef } from 'e2e-utils' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' -import { NextInstance } from 'test/lib/next-modes/base' -import { sandbox } from './helpers' +import { sandbox } from 'development-sandbox' +import { outdent } from 'outdent' describe('ReactRefreshModule app', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, }) - afterAll(() => next.destroy()) it('should allow any variable names', async () => { const { session, cleanup } = await sandbox(next, new Map([])) @@ -33,11 +28,13 @@ describe('ReactRefreshModule app', () => { for await (const variable of variables) { await session.patch( 'app/page.js', - `'use client' - import { default as ${variable} } from 'next/link' - export default function Page() { - return null - }` + outdent` + 'use client' + import { default as ${variable} } from 'next/link' + export default function Page() { + return null + } + ` ) expect(await session.hasRedbox(false)).toBe(false) expect(next.cliOutput).not.toContain( diff --git a/test/development/acceptance-app/ReactRefreshRegression.test.ts b/test/development/acceptance-app/ReactRefreshRegression.test.ts index 6c51fc3c442b..947ef8cd8bd3 100644 --- a/test/development/acceptance-app/ReactRefreshRegression.test.ts +++ b/test/development/acceptance-app/ReactRefreshRegression.test.ts @@ -1,27 +1,22 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' import { check } from 'next-test-utils' +import { outdent } from 'outdent' describe('ReactRefreshRegression app', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - skipStart: true, - dependencies: { - 'styled-components': '5.1.0', - '@next/mdx': 'canary', - '@mdx-js/loader': '0.18.0', - react: 'latest', - 'react-dom': 'latest', - }, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + 'styled-components': '5.1.0', + '@next/mdx': 'canary', + '@mdx-js/loader': '0.18.0', + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, }) - afterAll(() => next.destroy()) // https://github.com/vercel/next.js/issues/12422 // TODO-APP: port to app directory @@ -29,7 +24,7 @@ describe('ReactRefreshRegression app', () => { const files = new Map() files.set( 'pages/_document.js', - ` + outdent` import Document from 'next/document' import { ServerStyleSheet } from 'styled-components' @@ -67,7 +62,7 @@ describe('ReactRefreshRegression app', () => { // We start here. await session.patch( 'index.js', - ` + outdent` import React from 'react' import styled from 'styled-components' @@ -92,7 +87,8 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'app/page.js', - `'use client' + outdent` + 'use client' import { useCallback, useState } from 'react' export default function Index() { @@ -118,7 +114,8 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'app/page.js', - `'use client' + outdent` + 'use client' import { useCallback, useState } from 'react' export default function Index() { @@ -151,7 +148,7 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'app/page.js', - ` + outdent` export const revalidate = 0 import Component from '../index' @@ -162,7 +159,8 @@ describe('ReactRefreshRegression app', () => { ) await session.patch( 'index.js', - `'use client' + outdent` + 'use client' import { useCallback, useState } from 'react' export default function Index() { @@ -192,7 +190,8 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'index.js', - `'use client' + outdent` + 'use client' import { useCallback, useState } from 'react' export default function Index() { @@ -229,7 +228,7 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'app/page.js', - ` + outdent` export const config = {} import Component from '../index' @@ -241,7 +240,8 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'index.js', - `'use client' + outdent` + 'use client' import { useCallback, useState } from 'react' export const config = {} @@ -316,8 +316,10 @@ describe('ReactRefreshRegression app', () => { await session.patch( 'app/page.js', - `'use client' - export default function Page() { throw new Error('boom'); }` + outdent` + 'use client' + export default function Page() { throw new Error('boom'); } + ` ) expect(await session.hasRedbox(true)).toBe(true) @@ -337,7 +339,7 @@ describe('ReactRefreshRegression app', () => { const files = new Map() files.set( 'next.config.js', - ` + outdent` const withMDX = require("@next/mdx")({ extension: /\\.mdx?$/, }); @@ -350,12 +352,13 @@ describe('ReactRefreshRegression app', () => { files.set('app/content.mdx', `Hello World!`) files.set( 'app/page.js', - `'use client' - import MDX from './content.mdx' - export default function Page() { - return
- } - ` + outdent` + 'use client' + import MDX from './content.mdx' + export default function Page() { + return
+ } + ` ) const { session, cleanup } = await sandbox(next, files) diff --git a/test/development/acceptance-app/ReactRefreshRequire.test.ts b/test/development/acceptance-app/ReactRefreshRequire.test.ts index b09fb9ac25a7..adc38b72d156 100644 --- a/test/development/acceptance-app/ReactRefreshRequire.test.ts +++ b/test/development/acceptance-app/ReactRefreshRequire.test.ts @@ -1,23 +1,18 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext, FileRef } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' describe('ReactRefreshRequire app', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, }) - afterAll(() => next.destroy()) // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L989-L1048 test('re-runs accepted modules', async () => { @@ -87,12 +82,12 @@ describe('ReactRefreshRequire app', () => { await session.write( './foo.js', - ` - window.log.push('init FooV1'); - require('./bar'); + outdent` + window.log.push('init FooV1'); + require('./bar'); - // Exporting a component marks it as auto-accepting. - export default function Foo() {}; + // Exporting a component marks it as auto-accepting. + export default function Foo() {}; ` ) @@ -144,10 +139,10 @@ describe('ReactRefreshRequire app', () => { await session.evaluate(() => ((window as any).log = [])) await session.patch( './bar.js', - ` - window.log.push('init BarV3'); - // Exporting a component marks it as auto-accepting. - export default function Bar() {}; + outdent` + window.log.push('init BarV3'); + // Exporting a component marks it as auto-accepting. + export default function Bar() {}; ` ) expect(await session.evaluate(() => (window as any).log)).toEqual([ @@ -165,9 +160,9 @@ describe('ReactRefreshRequire app', () => { await session.evaluate(() => ((window as any).log = [])) await session.patch( './bar.js', - ` - window.log.push('init BarV4'); - export default function Bar() {}; + outdent` + window.log.push('init BarV4'); + export default function Bar() {}; ` ) expect(await session.evaluate(() => (window as any).log)).toEqual([ @@ -203,34 +198,34 @@ describe('ReactRefreshRequire app', () => { await session.write( 'root.js', - ` - window.log.push('init RootV1'); + outdent` + window.log.push('init RootV1'); - import './middleA'; - import './middleB'; - import './middleC'; + import './middleA'; + import './middleB'; + import './middleC'; - export default function Root() {}; + export default function Root() {}; ` ) await session.write( 'middleA.js', - ` - log.push('init MiddleAV1'); + outdent` + log.push('init MiddleAV1'); - import './leaf'; + import './leaf'; - export default function MiddleA() {}; + export default function MiddleA() {}; ` ) await session.write( 'middleB.js', - ` - log.push('init MiddleBV1'); + outdent` + log.push('init MiddleBV1'); - import './leaf'; + import './leaf'; - export default function MiddleB() {}; + export default function MiddleB() {}; ` ) // This one doesn't import leaf and also doesn't export a component (so it @@ -290,12 +285,12 @@ describe('ReactRefreshRequire app', () => { await session.evaluate(() => ((window as any).log = [])) await session.patch( 'middleB.js', - ` - log.push('init MiddleBV2'); + outdent` + log.push('init MiddleBV2'); - import './leaf'; + import './leaf'; - export default function MiddleB() {}; + export default function MiddleB() {}; ` ) expect(await session.evaluate(() => (window as any).log)).toEqual([ diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap index 5cf9d246fb05..ce10b2ebf4a8 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap @@ -4,9 +4,9 @@ exports[`ReactRefreshLogBox app default Module not found 1`] = ` "./index.js (1:0) Module not found: Can't resolve 'b' > 1 | import Comp from 'b' - 2 | export default function Oops() { - 3 | return ( - 4 |
+ 2 | export default function Oops() { + 3 | return ( + 4 |
https://nextjs.org/docs/messages/module-not-found @@ -15,33 +15,31 @@ Import trace for requested module: `; exports[`ReactRefreshLogBox app default Module not found empty import trace 1`] = ` -"./app/page.js (2:6) +"./app/page.js (2:0) Module not found: Can't resolve 'b' 1 | 'use client' -> 2 | import Comp from 'b' - | ^ - 3 | export default function Oops() { - 4 | return ( - 5 |
+> 2 | import Comp from 'b' + 3 | export default function Oops() { + 4 | return ( + 5 |
https://nextjs.org/docs/messages/module-not-found" `; exports[`ReactRefreshLogBox app default Module not found missing global CSS 1`] = ` -"./app/page.js:2:10 +"./app/page.js:2:0 Module not found: Can't resolve './non-existent.css' 1 | 'use client' -> 2 | import './non-existent.css' - | ^ - 3 | export default function Page(props) { - 4 | return

index page

- 5 | } +> 2 | import './non-existent.css' + 3 | export default function Page(props) { + 4 | return

index page

+ 5 | } https://nextjs.org/docs/messages/module-not-found" `; exports[`ReactRefreshLogBox app default Node.js builtins 1`] = ` -"./node_modules/my-package/index.js (2:0) +"./node_modules/my-package/index.js (1:0) Module not found: Can't resolve 'dns' https://nextjs.org/docs/messages/module-not-found diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap index 7734151a090b..48b1e8f6b495 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -22,15 +22,15 @@ Import trace for requested module: `; exports[`ReactRefreshLogBox app default Should not show __webpack_exports__ when exporting anonymous arrow function 1`] = ` -"index.js (4:16) @ default - - 2 | export default () => { - 3 | if (typeof window !== 'undefined') { -> 4 | throw new Error('test') - | ^ - 5 | } - 6 | - 7 | return null" +"index.js (3:10) @ default + + 1 | export default () => { + 2 | if (typeof window !== 'undefined') { +> 3 | throw new Error('test') + | ^ + 4 | } + 5 | + 6 | return null" `; exports[`ReactRefreshLogBox app default boundaries 1`] = ` @@ -41,15 +41,14 @@ exports[`ReactRefreshLogBox app default boundaries 1`] = ` `; exports[`ReactRefreshLogBox app default conversion to class component (1) 1`] = ` -"Child.js (5:18) @ ClickCount.render - - 3 | export default class ClickCount extends Component { - 4 | render() { -> 5 | throw new Error() - | ^ - 6 | } - 7 | } - 8 | " +"Child.js (4:10) @ ClickCount.render + + 2 | export default class ClickCount extends Component { + 3 | render() { +> 4 | throw new Error() + | ^ + 5 | } + 6 | }" `; exports[`ReactRefreshLogBox app default css syntax errors 1`] = ` @@ -77,25 +76,25 @@ exports[`ReactRefreshLogBox app default logbox: anchors links in error messages exports[`ReactRefreshLogBox app default logbox: anchors links in error messages 9`] = `"http://example.com/"`; exports[`ReactRefreshLogBox app default module init error not shown 1`] = ` -"index.js (4:14) @ eval - - 2 | // top offset for snapshot - 3 | import * as React from 'react'; -> 4 | throw new Error('no') - | ^ - 5 | class ClassDefault extends React.Component { - 6 | render() { - 7 | return

Default Export

;" +"index.js (3:6) @ eval + + 1 | // top offset for snapshot + 2 | import * as React from 'react'; +> 3 | throw new Error('no') + | ^ + 4 | class ClassDefault extends React.Component { + 5 | render() { + 6 | return

Default Export

;" `; exports[`ReactRefreshLogBox app default should strip whitespace correctly with newline 1`] = ` -"index.js (9:34) @ onClick +"index.js (8:26) @ onClick - 7 | - 8 | { -> 9 | throw new Error('idk') + 6 | + 7 | { +> 8 | throw new Error('idk') | ^ - 10 | }}> - 11 | click me - 12 | " + 9 | }}> + 10 | click me + 11 | " `; diff --git a/test/development/acceptance-app/app-hmr-changes.test.ts b/test/development/acceptance-app/app-hmr-changes.test.ts index a4a2ade3c45f..a146838ef2a0 100644 --- a/test/development/acceptance-app/app-hmr-changes.test.ts +++ b/test/development/acceptance-app/app-hmr-changes.test.ts @@ -1,10 +1,9 @@ -import { createNextDescribe, FileRef } from 'e2e-utils' +import { FileRef, nextTestSetup } from 'e2e-utils' import { check, hasRedbox, waitFor, retry } from 'next-test-utils' import path from 'path' -createNextDescribe( - 'Error overlay - RSC build errors', - { +describe('Error overlay - RSC build errors', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'app-hmr-changes')), dependencies: { '@next/mdx': 'canary', @@ -18,48 +17,47 @@ createNextDescribe( 'image-size': '^1.0.2', autoprefixer: '^10.4.13', }, - }, - ({ next }) => { - it('should handle successive HMR changes with errors correctly', async () => { - const browser = await retry( - () => next.browser('/2020/develop-preview-test'), - 1000, - 500 - ) + }) - await check( - () => browser.eval('document.documentElement.innerHTML'), - /A few years ago I tweeted/ - ) + it('should handle successive HMR changes with errors correctly', async () => { + const browser = await retry( + () => next.browser('/2020/develop-preview-test'), + 1000, + 500 + ) - const pagePath = 'app/(post)/2020/develop-preview-test/page.mdx' - const originalPage = await next.readFile(pagePath) + await check( + () => browser.eval('document.documentElement.innerHTML'), + /A few years ago I tweeted/ + ) - const break1 = originalPage.replace('break 1', '
') + const pagePath = 'app/(post)/2020/develop-preview-test/page.mdx' + const originalPage = await next.readFile(pagePath) - await next.patchFile(pagePath, break1) - - const break2 = break1.replace('{/* break point 2 */}', '
') + const break1 = originalPage.replace('break 1', '
') - await next.patchFile(pagePath, break2) + await next.patchFile(pagePath, break1) - for (let i = 0; i < 5; i++) { - await next.patchFile(pagePath, break2.replace('break 3', '')) + const break2 = break1.replace('{/* break point 2 */}', '
') - await next.patchFile(pagePath, break2) - expect(await hasRedbox(browser, true)).toBe(true) + await next.patchFile(pagePath, break2) - await next.patchFile(pagePath, break1) - await waitFor(100) + for (let i = 0; i < 5; i++) { + await next.patchFile(pagePath, break2.replace('break 3', '')) - await next.patchFile(pagePath, originalPage) - expect(await hasRedbox(browser, false)).toBe(false) - } + await next.patchFile(pagePath, break2) + expect(await hasRedbox(browser, true)).toBe(true) - await check( - () => browser.eval('document.documentElement.innerHTML'), - /A few years ago I tweeted/ - ) - }) - } -) + await next.patchFile(pagePath, break1) + await waitFor(100) + + await next.patchFile(pagePath, originalPage) + expect(await hasRedbox(browser, false)).toBe(false) + } + + await check( + () => browser.eval('document.documentElement.innerHTML'), + /A few years ago I tweeted/ + ) + }) +}) diff --git a/test/development/acceptance-app/component-stack.test.ts b/test/development/acceptance-app/component-stack.test.ts index 70a6f42a3052..94e93e37fec2 100644 --- a/test/development/acceptance-app/component-stack.test.ts +++ b/test/development/acceptance-app/component-stack.test.ts @@ -1,63 +1,62 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe, FileRef } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' -createNextDescribe( - 'Component Stack in error overlay', - { +describe('Component Stack in error overlay', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should show a component stack on hydration error', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/component.js', - ` - 'use client' - const isClient = typeof window !== 'undefined' - export default function Component() { - return ( -
-

{isClient ? "client" : "server"}

-
- ); - } -`, - ], - [ - 'app/page.js', - ` - import Component from './component' - export default function Mismatch() { - return ( -
- -
- ); - } -`, - ], - ]) - ) + }) - await session.waitForAndOpenRuntimeError() + it('should show a component stack on hydration error', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/component.js', + outdent` + 'use client' + const isClient = typeof window !== 'undefined' + export default function Component() { + return ( +
+

{isClient ? "client" : "server"}

+
+ ); + } + `, + ], + [ + 'app/page.js', + outdent` + import Component from './component' + export default function Mismatch() { + return ( +
+ +
+ ); + } + `, + ], + ]) + ) - expect(await session.getRedboxComponentStack()).toMatchInlineSnapshot(` + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxComponentStack()).toMatchInlineSnapshot(` "p div Component main" `) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/dynamic-error.test.ts b/test/development/acceptance-app/dynamic-error.test.ts index 21c38dafa228..4cf03fceeab6 100644 --- a/test/development/acceptance-app/dynamic-error.test.ts +++ b/test/development/acceptance-app/dynamic-error.test.ts @@ -1,22 +1,22 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' -createNextDescribe( - 'dynamic = "error" in devmode', - { - files: path.join(__dirname, 'fixtures', 'default-template'), +describe('dynamic = "error" in devmode', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), skipStart: true, - }, - ({ next }) => { - it('should show error overlay when dynamic is forced', async () => { - const { session, cleanup } = await sandbox(next, undefined, '/server') - - // dynamic = "error" and force dynamic - await session.patch( - 'app/server/page.js', - ` + }) + + it('should show error overlay when dynamic is forced', async () => { + const { session, cleanup } = await sandbox(next, undefined, '/server') + + // dynamic = "error" and force dynamic + await session.patch( + 'app/server/page.js', + outdent` import { cookies } from 'next/headers'; import Component from '../../index' @@ -28,15 +28,14 @@ createNextDescribe( export const dynamic = "error" ` - ) - - await session.hasRedbox(true) - console.log(await session.getRedboxDescription()) - expect(await session.getRedboxDescription()).toBe( - `Error: Page with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`.` - ) - - await cleanup() - }) - } -) + ) + + await session.hasRedbox(true) + console.log(await session.getRedboxDescription()) + expect(await session.getRedboxDescription()).toBe( + `Error: Page with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`.` + ) + + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/editor-links.test.ts b/test/development/acceptance-app/editor-links.test.ts index 0827881b3fe1..448684208e2d 100644 --- a/test/development/acceptance-app/editor-links.test.ts +++ b/test/development/acceptance-app/editor-links.test.ts @@ -1,7 +1,8 @@ import { check } from 'next-test-utils' -import { createNextDescribe, FileRef } from 'e2e-utils' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' -import { sandbox } from './helpers' +import { sandbox } from 'development-sandbox' +import { outdent } from 'outdent' async function clickSourceFile(browser: any) { await browser.waitForElementByCss( @@ -24,136 +25,140 @@ async function clickImportTraceFiles(browser: any) { } } -createNextDescribe( - 'Error overlay - editor links', - { +describe('Error overlay - editor links', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should be possible to open source file on build error', async () => { - let editorRequestsCount = 0 - const { session, browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` + }) + + it('should be possible to open source file on build error', async () => { + let editorRequestsCount = 0 + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` import Component from '../index' export default function Page() { return } `, - ], - ]), - undefined, - { - beforePageLoad(page) { - page.route('**/__nextjs_launch-editor**', (route) => { - editorRequestsCount += 1 - route.fulfill() - }) - }, - } - ) + ], + ]), + undefined, + { + beforePageLoad(page) { + page.route('**/__nextjs_launch-editor**', (route) => { + editorRequestsCount += 1 + route.fulfill() + }) + }, + } + ) - await session.patch( - 'index.js', - `import { useState } from 'react' - export default () => 'hello world'` - ) + await session.patch( + 'index.js', + outdent` + import { useState } from 'react' + export default () => 'hello world' + ` + ) - expect(await session.hasRedbox(true)).toBe(true) - await clickSourceFile(browser) - await check(() => editorRequestsCount, /1/) + expect(await session.hasRedbox(true)).toBe(true) + await clickSourceFile(browser) + await check(() => editorRequestsCount, /1/) - await cleanup() - }) + await cleanup() + }) - it('should be possible to open import trace files on RSC parse error', async () => { - let editorRequestsCount = 0 - const { session, browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - import Component from '../index' - export default function Page() { - return - } - `, - ], - ['mod1.js', "import './mod2.js'"], - ['mod2.js', '{{{{{'], - ]), - undefined, - { - beforePageLoad(page) { - page.route('**/__nextjs_launch-editor**', (route) => { - editorRequestsCount += 1 - route.fulfill() - }) - }, - } - ) + it('should be possible to open import trace files on RSC parse error', async () => { + let editorRequestsCount = 0 + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + import Component from '../index' + export default function Page() { + return + } + `, + ], + ['mod1.js', "import './mod2.js'"], + ['mod2.js', '{{{{{'], + ]), + undefined, + { + beforePageLoad(page) { + page.route('**/__nextjs_launch-editor**', (route) => { + editorRequestsCount += 1 + route.fulfill() + }) + }, + } + ) - await session.patch( - 'index.js', - `import './mod1' - export default () => 'hello world'` - ) + await session.patch( + 'index.js', + outdent` + import './mod1' + export default () => 'hello world' + ` + ) - expect(await session.hasRedbox(true)).toBe(true) - await clickImportTraceFiles(browser) - await check(() => editorRequestsCount, /4/) + expect(await session.hasRedbox(true)).toBe(true) + await clickImportTraceFiles(browser) + await check(() => editorRequestsCount, /4/) - await cleanup() - }) + await cleanup() + }) - it('should be possible to open import trace files on module not found error', async () => { - let editorRequestsCount = 0 - const { session, browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - import Component from '../index' - export default function Page() { - return - } - `, - ], - ['mod1.js', "import './mod2.js'"], - ['mod2.js', 'import "boom"'], - ]), - undefined, - { - beforePageLoad(page) { - page.route('**/__nextjs_launch-editor**', (route) => { - editorRequestsCount += 1 - route.fulfill() - }) - }, - } - ) + it('should be possible to open import trace files on module not found error', async () => { + let editorRequestsCount = 0 + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + import Component from '../index' + export default function Page() { + return + } + `, + ], + ['mod1.js', "import './mod2.js'"], + ['mod2.js', 'import "boom"'], + ]), + undefined, + { + beforePageLoad(page) { + page.route('**/__nextjs_launch-editor**', (route) => { + editorRequestsCount += 1 + route.fulfill() + }) + }, + } + ) - await session.patch( - 'index.js', - `import './mod1' - export default () => 'hello world'` - ) + await session.patch( + 'index.js', + outdent` + import './mod1' + export default () => 'hello world' + ` + ) - expect(await session.hasRedbox(true)).toBe(true) - await clickImportTraceFiles(browser) - await check(() => editorRequestsCount, /3/) + expect(await session.hasRedbox(true)).toBe(true) + await clickImportTraceFiles(browser) + await check(() => editorRequestsCount, /3/) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/error-message-url.test.ts b/test/development/acceptance-app/error-message-url.test.ts index b18a96a40399..e27010f86343 100644 --- a/test/development/acceptance-app/error-message-url.test.ts +++ b/test/development/acceptance-app/error-message-url.test.ts @@ -1,73 +1,71 @@ -import { createNextDescribe, FileRef } from 'e2e-utils' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' -import { sandbox } from './helpers' +import { sandbox } from 'development-sandbox' +import { outdent } from 'outdent' -createNextDescribe( - 'Error overlay - error message urls', - { +describe('Error overlay - error message urls', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should be possible to click url in build error', async () => { - const { session, browser, cleanup } = await sandbox(next) + }) - const content = await next.readFile('app/page.js') + it('should be possible to click url in build error', async () => { + const { session, browser, cleanup } = await sandbox(next) - await session.patch( - 'app/page.js', - content + '\nexport function getServerSideProps() {}' - ) + const content = await next.readFile('app/page.js') - expect(await session.hasRedbox(true)).toBe(true) + await session.patch( + 'app/page.js', + content + '\nexport function getServerSideProps() {}' + ) - const link = await browser.elementByCss('[data-nextjs-terminal] a') - const text = await link.text() - const href = await link.getAttribute('href') - expect(text).toEqual( - 'https://nextjs.org/docs/app/building-your-application/data-fetching' - ) - expect(href).toEqual( - 'https://nextjs.org/docs/app/building-your-application/data-fetching' - ) + expect(await session.hasRedbox(true)).toBe(true) - await cleanup() - }) + const link = await browser.elementByCss('[data-nextjs-terminal] a') + const text = await link.text() + const href = await link.getAttribute('href') + expect(text).toEqual( + 'https://nextjs.org/docs/app/building-your-application/data-fetching' + ) + expect(href).toEqual( + 'https://nextjs.org/docs/app/building-your-application/data-fetching' + ) - it('should be possible to click url in runtime error', async () => { - const { session, browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - `'use client' + await cleanup() + }) + + it('should be possible to click url in runtime error', async () => { + const { session, browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' export default function Page() { return typeof window === 'undefined' ? 'HELLO' : 'WORLD' } - `, - ], - ]) - ) + `, + ], + ]) + ) - await session.waitForAndOpenRuntimeError() + await session.waitForAndOpenRuntimeError() - const link = await browser.elementByCss( - '#nextjs__container_errors_desc a' - ) - const text = await link.text() - const href = await link.getAttribute('href') - expect(text).toEqual( - 'https://nextjs.org/docs/messages/react-hydration-error' - ) - expect(href).toEqual( - 'https://nextjs.org/docs/messages/react-hydration-error' - ) + const link = await browser.elementByCss('#nextjs__container_errors_desc a') + const text = await link.text() + const href = await link.getAttribute('href') + expect(text).toEqual( + 'https://nextjs.org/docs/messages/react-hydration-error' + ) + expect(href).toEqual( + 'https://nextjs.org/docs/messages/react-hydration-error' + ) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/error-recovery.test.ts b/test/development/acceptance-app/error-recovery.test.ts index 20ffdf440a1f..5315ae453c2b 100644 --- a/test/development/acceptance-app/error-recovery.test.ts +++ b/test/development/acceptance-app/error-recovery.test.ts @@ -1,30 +1,26 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe, FileRef } from 'e2e-utils' -import { check, getSnapshotTestDescribe } from 'next-test-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { check, describeVariants as describe } from 'next-test-utils' import path from 'path' +import { outdent } from 'outdent' + +describe.each(['default', 'turbo'])('Error recovery app %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', + }, + skipStart: true, + }) + + test('can recover from a syntax error without losing state', async () => { + const { session, cleanup } = await sandbox(next) -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`Error recovery app ${variant}`, () => { - createNextDescribe( - 'Error recovery with overlay', - { - files: new FileRef( - path.join(__dirname, 'fixtures', 'default-template') - ), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, - }, - ({ next }) => { - test('can recover from a syntax error without losing state', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import { useCallback, useState } from 'react' export default function Index() { @@ -38,25 +34,23 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - await session.evaluate(() => document.querySelector('button').click()) - expect( - await session.evaluate( - () => document.querySelector('p').textContent - ) - ).toBe('1') + await session.evaluate(() => document.querySelector('button').click()) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('1') - await session.patch('index.js', `export default () =>
- session.evaluate(() => document.querySelector('p').textContent), - /Count: 1/ - ) + ) - await cleanup() - }) + expect(await session.hasRedbox(false)).toBe(false) - test.each([['client'], ['server']])( - '%s component can recover from syntax error', - async (type: string) => { - const { session, browser, cleanup } = await sandbox( - next, - undefined, - '/' + type - ) + await check( + () => session.evaluate(() => document.querySelector('p').textContent), + /Count: 1/ + ) - // Add syntax error - await session.patch( - `app/${type}/page.js`, - ` - export default function Page() { - return

Hello world

-` - ) - expect(await session.hasRedbox(true)).toBe(true) - - // Fix syntax error - await session.patch( - `app/${type}/page.js`, - ` - export default function Page() { - return

Hello world 2

- } -` - ) + await cleanup() + }) - await check(() => browser.elementByCss('p').text(), 'Hello world 2') - await cleanup() + test.each([['client'], ['server']])( + '%s component can recover from syntax error', + async (type: string) => { + const { session, browser, cleanup } = await sandbox( + next, + undefined, + '/' + type + ) + + // Add syntax error + await session.patch( + `app/${type}/page.js`, + outdent` + export default function Page() { + return

Hello world

+ ` + ) + expect(await session.hasRedbox(true)).toBe(true) + + // Fix syntax error + await session.patch( + `app/${type}/page.js`, + outdent` + export default function Page() { + return

Hello world 2

} - ) - - test('can recover from a event handler error', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` - import { useCallback, useState } from 'react' - - export default function Index() { - const [count, setCount] = useState(0) - const increment = useCallback(() => { - setCount(c => c + 1) - throw new Error('oops') - }, [setCount]) - return ( -
-

{count}

- -
- ) - } - ` - ) + ` + ) - expect( - await session.evaluate( - () => document.querySelector('p').textContent - ) - ).toBe('0') - await session.evaluate(() => document.querySelector('button').click()) - expect( - await session.evaluate( - () => document.querySelector('p').textContent - ) - ).toBe('1') - - await session.waitForAndOpenRuntimeError() - expect(await session.getRedboxSource()).toMatchInlineSnapshot(` - "index.js (8:22) @ eval - - 6 | const increment = useCallback(() => { - 7 | setCount(c => c + 1) - > 8 | throw new Error('oops') - | ^ - 9 | }, [setCount]) - 10 | return ( - 11 |
" - `) - - await session.patch( - 'index.js', - ` - import { useCallback, useState } from 'react' - - export default function Index() { - const [count, setCount] = useState(0) - const increment = useCallback(() => setCount(c => c + 1), [setCount]) - return ( -
-

Count: {count}

- -
- ) - } - ` - ) + await check(() => browser.elementByCss('p').text(), 'Hello world 2') + await cleanup() + } + ) - expect(await session.hasRedbox(false)).toBe(false) - expect(await session.hasErrorToast()).toBe(false) + test('can recover from a event handler error', async () => { + const { session, cleanup } = await sandbox(next) - expect( - await session.evaluate( - () => document.querySelector('p').textContent - ) - ).toBe('Count: 1') - await session.evaluate(() => document.querySelector('button').click()) - expect( - await session.evaluate( - () => document.querySelector('p').textContent - ) - ).toBe('Count: 2') + await session.patch( + 'index.js', + outdent` + import { useCallback, useState } from 'react' - expect(await session.hasRedbox(false)).toBe(false) - expect(await session.hasErrorToast()).toBe(false) + export default function Index() { + const [count, setCount] = useState(0) + const increment = useCallback(() => { + setCount(c => c + 1) + throw new Error('oops') + }, [setCount]) + return ( +
+

{count}

+ +
+ ) + } + ` + ) - await cleanup() - }) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('0') + await session.evaluate(() => document.querySelector('button').click()) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('1') + + await session.waitForAndOpenRuntimeError() + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + "index.js (7:10) @ eval + + 5 | const increment = useCallback(() => { + 6 | setCount(c => c + 1) + > 7 | throw new Error('oops') + | ^ + 8 | }, [setCount]) + 9 | return ( + 10 |
" + `) + + await session.patch( + 'index.js', + outdent` + import { useCallback, useState } from 'react' - test.each([['client'], ['server']])( - '%s component can recover from a component error', - async (type: string) => { - const { session, cleanup, browser } = await sandbox( - next, - undefined, - '/' + type - ) + export default function Index() { + const [count, setCount] = useState(0) + const increment = useCallback(() => setCount(c => c + 1), [setCount]) + return ( +
+

Count: {count}

+ +
+ ) + } + ` + ) - await session.write( - 'child.js', - ` - export default function Child() { - return

Hello

; - } - ` - ) + expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasErrorToast()).toBe(false) - await session.patch( - 'index.js', - ` - import Child from './child' - - export default function Index() { - return ( -
- -
- ) - } - ` - ) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Count: 1') + await session.evaluate(() => document.querySelector('button').click()) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Count: 2') - expect(await browser.elementByCss('p').text()).toBe('Hello') + expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasErrorToast()).toBe(false) - await session.patch( - 'child.js', - ` - // hello - export default function Child() { - throw new Error('oops') - } - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - 'export default function Child()' - ) + await cleanup() + }) - // TODO-APP: re-enable when error recovery doesn't reload the page. - /* const didNotReload = */ await session.patch( - 'child.js', - ` - export default function Child() { - return

Hello

; - } - ` + test.each([['client'], ['server']])( + '%s component can recover from a component error', + async (type: string) => { + const { session, cleanup, browser } = await sandbox( + next, + undefined, + '/' + type + ) + + await session.write( + 'child.js', + outdent` + export default function Child() { + return

Hello

; + } + ` + ) + + await session.patch( + 'index.js', + outdent` + import Child from './child' + + export default function Index() { + return ( +
+ +
) - - // TODO-APP: re-enable when error recovery doesn't reload the page. - // expect(didNotReload).toBe(true) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate( - () => document.querySelector('p').textContent - ) - ).toBe('Hello') - - await cleanup() } - ) + ` + ) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098 - test('syntax > runtime error', async () => { - const { session, cleanup } = await sandbox(next) + expect(await browser.elementByCss('p').text()).toBe('Hello') - // Start here. - await session.patch( - 'index.js', - ` + await session.patch( + 'child.js', + outdent` + // hello + export default function Child() { + throw new Error('oops') + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + 'export default function Child()' + ) + + // TODO-APP: re-enable when error recovery doesn't reload the page. + /* const didNotReload = */ await session.patch( + 'child.js', + outdent` + export default function Child() { + return

Hello

; + } + ` + ) + + // TODO-APP: re-enable when error recovery doesn't reload the page. + // expect(didNotReload).toBe(true) + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Hello') + + await cleanup() + } + ) + + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098 + test('syntax > runtime error', async () => { + const { session, cleanup } = await sandbox(next) + + // Start here. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; export default function FunctionNamed() { return
} ` - ) - // TODO: this acts weird without above step - await session.patch( - 'index.js', - ` + ) + // TODO: this acts weird without above step + await session.patch( + 'index.js', + outdent` import * as React from 'react'; let i = 0 setInterval(() => { @@ -307,51 +290,52 @@ for (const variant of ['default', 'turbo']) { return
} ` - ) + ) - await new Promise((resolve) => setTimeout(resolve, 1000)) - await session.waitForAndOpenRuntimeError() - expect(await session.getRedboxSource()).not.toInclude( - "Expected '}', got ''" - ) + await new Promise((resolve) => setTimeout(resolve, 1000)) + await session.waitForAndOpenRuntimeError() + expect(await session.getRedboxSource()).not.toInclude( + "Expected '}', got ''" + ) - // Make a syntax error. - await session.patch( - 'index.js', - ` + // Make a syntax error. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; let i = 0 setInterval(() => { i++ throw Error('no ' + i) }, 1000) - export default function FunctionNamed() {` - ) + export default function FunctionNamed() { + ` + ) - await new Promise((resolve) => setTimeout(resolve, 1000)) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - "Expected '}', got ''" - ) + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + "Expected '}', got ''" + ) - // Test that runtime error does not take over: - await new Promise((resolve) => setTimeout(resolve, 2000)) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - "Expected '}', got ''" - ) + // Test that runtime error does not take over: + await new Promise((resolve) => setTimeout(resolve, 2000)) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + "Expected '}', got ''" + ) - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016 - test('stuck error', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016 + test('stuck error', async () => { + const { session, cleanup } = await sandbox(next) - // We start here. - await session.patch( - 'index.js', - ` + // We start here. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; function FunctionDefault() { @@ -360,23 +344,23 @@ for (const variant of ['default', 'turbo']) { export default FunctionDefault; ` - ) + ) - // We add a new file. Let's call it Foo.js. - await session.write( - 'Foo.js', - ` + // We add a new file. Let's call it Foo.js. + await session.write( + 'Foo.js', + outdent` // intentionally skips export export default function Foo() { return React.createElement('h1', null, 'Foo'); } ` - ) + ) - // We edit our first file to use it. - await session.patch( - 'index.js', - ` + // We edit our first file to use it. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; import Foo from './Foo'; function FunctionDefault() { @@ -384,39 +368,39 @@ for (const variant of ['default', 'turbo']) { } export default FunctionDefault; ` - ) + ) - // We get an error because Foo didn't import React. Fair. - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - "return React.createElement('h1', null, 'Foo');" - ) + // We get an error because Foo didn't import React. Fair. + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + "return React.createElement('h1', null, 'Foo');" + ) - // Let's add that to Foo. - await session.patch( - 'Foo.js', - ` + // Let's add that to Foo. + await session.patch( + 'Foo.js', + outdent` import * as React from 'react'; export default function Foo() { return React.createElement('h1', null, 'Foo'); } ` - ) + ) - // Expected: this fixes the problem - expect(await session.hasRedbox(false)).toBe(false) + // Expected: this fixes the problem + expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262 - test('render error not shown right after syntax error', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262 + test('render error not shown right after syntax error', async () => { + const { session, cleanup } = await sandbox(next) - // Starting here: - await session.patch( - 'index.js', - ` + // Starting here: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { render() { @@ -426,18 +410,16 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) + ) - expect( - await session.evaluate( - () => document.querySelector('h1').textContent - ) - ).toBe('Default Export') + expect( + await session.evaluate(() => document.querySelector('h1').textContent) + ).toBe('Default Export') - // Break it with a syntax error: - await session.patch( - 'index.js', - ` + // Break it with a syntax error: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { @@ -448,13 +430,13 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) - expect(await session.hasRedbox(true)).toBe(true) + ) + expect(await session.hasRedbox(true)).toBe(true) - // Now change the code to introduce a runtime error without fixing the syntax error: - await session.patch( - 'index.js', - ` + // Now change the code to introduce a runtime error without fixing the syntax error: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { @@ -466,13 +448,13 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) - expect(await session.hasRedbox(true)).toBe(true) + ) + expect(await session.hasRedbox(true)).toBe(true) - // Now fix the syntax error: - await session.patch( - 'index.js', - ` + // Now fix the syntax error: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { @@ -484,33 +466,30 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) - expect(await session.hasRedbox(true)).toBe(true) + ) + expect(await session.hasRedbox(true)).toBe(true) - await check(async () => { - const source = await session.getRedboxSource() - return source?.includes('render() {') ? 'success' : source - }, 'success') + await check(async () => { + const source = await session.getRedboxSource() + return source?.includes('render() {') ? 'success' : source + }, 'success') - expect(await session.getRedboxSource()).toInclude( - "throw new Error('nooo');" - ) + expect(await session.getRedboxSource()).toInclude( + "throw new Error('nooo');" + ) - await cleanup() - }) + await cleanup() + }) - test('displays build error on initial page load', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([['app/page.js', '{{{']]) - ) + test('displays build error on initial page load', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([['app/page.js', '{{{']]) + ) - expect(await session.hasRedbox(true)).toBe(true) - await check(() => session.getRedboxSource(true), /Failed to compile/) + expect(await session.hasRedbox(true)).toBe(true) + await check(() => session.getRedboxSource(true), /Failed to compile/) - await cleanup() - }) - } - ) + await cleanup() }) -} +}) diff --git a/test/development/acceptance-app/hydration-error.test.ts b/test/development/acceptance-app/hydration-error.test.ts index 35f88fd10c7d..213c8e1e667e 100644 --- a/test/development/acceptance-app/hydration-error.test.ts +++ b/test/development/acceptance-app/hydration-error.test.ts @@ -1,44 +1,45 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe, FileRef } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' // https://github.com/facebook/react/blob/main/packages/react-dom/src/__tests__/ReactDOMHydrationDiff-test.js used as a reference -createNextDescribe( - 'Error overlay for hydration errors', - { + +describe('Error overlay for hydration errors', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should show correct hydration error when client and server render different text', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - 'use client' - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return ( -
-
{isClient ? "client" : "server"}
-
- ); - } -`, - ], - ]) - ) - - await session.waitForAndOpenRuntimeError() - - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + }) + + it('should show correct hydration error when client and server render different text', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return ( +
+
{isClient ? "client" : "server"}
+
+ ); + } + `, + ], + ]) + ) + + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Text content does not match server-rendered HTML. Warning: Text content did not match. Server: \\"server\\" Client: \\"client\\" @@ -46,33 +47,33 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - - it('should show correct hydration error when client renders an extra element', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - 'use client' - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return ( -
- {isClient &&
} -
- ); - } -`, - ], - ]) - ) - - await session.waitForAndOpenRuntimeError() - - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + await cleanup() + }) + + it('should show correct hydration error when client renders an extra element', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return ( +
+ {isClient &&
} +
+ ); + } + `, + ], + ]) + ) + + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching
in
. @@ -80,34 +81,34 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - it('should show correct hydration error when client renders an extra text node', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - 'use client' - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return ( -
-
- {isClient && "second"} -
-
- ); - } -`, - ], - ]) - ) - - await session.waitForAndOpenRuntimeError() - - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + await cleanup() + }) + it('should show correct hydration error when client renders an extra text node', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return ( +
+
+ {isClient && "second"} +
+
+ ); + } + `, + ], + ]) + ) + + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching text node for \\"second\\" in
. @@ -115,33 +116,33 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - - it('should show correct hydration error when server renders an extra element', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - 'use client' - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return ( -
- {!isClient &&
} -
- ); - } -`, - ], - ]) - ) - - await session.waitForAndOpenRuntimeError() - - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + await cleanup() + }) + + it('should show correct hydration error when server renders an extra element', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return ( +
+ {!isClient &&
} +
+ ); + } + `, + ], + ]) + ) + + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Did not expect server HTML to contain a
in
. @@ -149,29 +150,29 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - - it('should show correct hydration error when server renders an extra text node', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - 'use client' - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return
{!isClient && "only"}
; - } -`, - ], - ]) - ) - - await session.waitForAndOpenRuntimeError() - - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + await cleanup() + }) + + it('should show correct hydration error when server renders an extra text node', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return
{!isClient && "only"}
; + } + `, + ], + ]) + ) + + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Did not expect server HTML to contain the text node \\"only\\" in
. @@ -179,38 +180,38 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - - it('should show correct hydration error when client renders an extra node inside Suspense content', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - 'use client' - import React from "react" - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return ( -
- Loading...

}> -
- {isClient &&
} -
- -
- ); - } -`, - ], - ]) - ) - - await session.waitForAndOpenRuntimeError() - - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + await cleanup() + }) + + it('should show correct hydration error when client renders an extra node inside Suspense content', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + 'use client' + import React from "react" + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return ( +
+ Loading...

}> +
+ {isClient &&
} +
+ +
+ ); + } + `, + ], + ]) + ) + + await session.waitForAndOpenRuntimeError() + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching
in
. @@ -218,7 +219,6 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/invalid-imports.test.ts b/test/development/acceptance-app/invalid-imports.test.ts index ca6cf8f19b8a..278216e2ef8c 100644 --- a/test/development/acceptance-app/invalid-imports.test.ts +++ b/test/development/acceptance-app/invalid-imports.test.ts @@ -1,11 +1,11 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe, FileRef } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' -createNextDescribe( - 'Error Overlay invalid imports', - { +describe('Error Overlay invalid imports', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', @@ -14,25 +14,25 @@ createNextDescribe( 'client-only': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should show error when using styled-jsx in server component', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/comp1.js', - ` - import { Comp2 } from './comp2' - - export function Comp1() { - return - } + }) + + it('should show error when using styled-jsx in server component', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/comp1.js', + outdent` + import { Comp2 } from './comp2' + + export function Comp1() { + return + } `, - ], - [ - 'app/comp2.js', - ` + ], + [ + 'app/comp2.js', + outdent` export function Comp2() { return (
@@ -44,29 +44,29 @@ createNextDescribe(
) } - `, - ], - [ - 'app/page.js', - `'use client' - import { Comp1 } from './comp1' - - export default function Page() { - return - } + ], + [ + 'app/page.js', + outdent` + 'use client' + import { Comp1 } from './comp1' + + export default function Page() { + return + } `, - ], - ]) - ) + ], + ]) + ) - const pageFile = 'app/page.js' - const content = await next.readFile(pageFile) - const withoutUseClient = content.replace("'use client'", '') - await session.patch(pageFile, withoutUseClient) + const pageFile = 'app/page.js' + const content = await next.readFile(pageFile) + const withoutUseClient = content.replace("'use client'", '') + await session.patch(pageFile, withoutUseClient) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` "./app/comp2.js 'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component. @@ -78,70 +78,70 @@ createNextDescribe( ./app/page.js" `) - await cleanup() - }) + await cleanup() + }) - it('should show error when external package imports client-only in server component', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/client-only-package/index.js', - ` + it('should show error when external package imports client-only in server component', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/client-only-package/index.js', + outdent` require("client-only") `, - ], - [ - 'node_modules/client-only-package/package.json', - ` + ], + [ + 'node_modules/client-only-package/package.json', + outdent` { "name": "client-only-package", "main": "index.js" } `, - ], - [ - 'app/comp1.js', - ` - import { Comp2 } from './comp2' - - export function Comp1() { - return - } + ], + [ + 'app/comp1.js', + outdent` + import { Comp2 } from './comp2' + + export function Comp1() { + return + } `, - ], - [ - 'app/comp2.js', - ` + ], + [ + 'app/comp2.js', + outdent` import "client-only-package" export function Comp2() { return (
Hello world
) } - `, - ], - [ - 'app/page.js', - `'use client' - import { Comp1 } from './comp1' - - export default function Page() { - return - } + ], + [ + 'app/page.js', + outdent` + 'use client' + import { Comp1 } from './comp1' + + export default function Page() { + return + } `, - ], - ]) - ) + ], + ]) + ) - const pageFile = 'app/page.js' - const content = await next.readFile(pageFile) - const withoutUseClient = content.replace("'use client'", '') - await session.patch(pageFile, withoutUseClient) + const pageFile = 'app/page.js' + const content = await next.readFile(pageFile) + const withoutUseClient = content.replace("'use client'", '') + await session.patch(pageFile, withoutUseClient) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` "./app/comp2.js 'client-only' cannot be imported from a Server Component module. It should only be used from a Client Component. @@ -153,68 +153,68 @@ createNextDescribe( ./app/page.js" `) - await cleanup() - }) + await cleanup() + }) - it('should show error when external package imports server-only in client component', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/server-only-package/index.js', - ` + it('should show error when external package imports server-only in client component', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/server-only-package/index.js', + outdent` require("server-only") `, - ], - [ - 'node_modules/server-only-package/package.json', - ` + ], + [ + 'node_modules/server-only-package/package.json', + outdent` { "name": "server-only-package", "main": "index.js" } `, - ], - [ - 'app/comp1.js', - ` - import { Comp2 } from './comp2' - - export function Comp1() { - return - } + ], + [ + 'app/comp1.js', + outdent` + import { Comp2 } from './comp2' + + export function Comp1() { + return + } `, - ], - [ - 'app/comp2.js', - ` + ], + [ + 'app/comp2.js', + outdent` import 'server-only-package' export function Comp2() { return (
Hello world
) } - `, - ], - [ - 'app/page.js', - `import { Comp1 } from './comp1' - - export default function Page() { - return - } + ], + [ + 'app/page.js', + outdent` + import { Comp1 } from './comp1' + + export default function Page() { + return + } `, - ], - ]) - ) + ], + ]) + ) - const file = 'app/page.js' - const content = await next.readFile(file) - await session.patch(file, "'use client'\n" + content) + const file = 'app/page.js' + const content = await next.readFile(file) + await session.patch(file, "'use client'\n" + content) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` "./app/comp2.js 'server-only' cannot be imported from a Client Component module. It should only be used from a Server Component. @@ -226,7 +226,6 @@ createNextDescribe( ./app/page.js" `) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/rsc-build-errors.test.ts b/test/development/acceptance-app/rsc-build-errors.test.ts index becbd0863e07..8b620be64351 100644 --- a/test/development/acceptance-app/rsc-build-errors.test.ts +++ b/test/development/acceptance-app/rsc-build-errors.test.ts @@ -1,282 +1,282 @@ -import { createNextDescribe, FileRef } from 'e2e-utils' +import { FileRef, nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' import path from 'path' -import { sandbox } from './helpers' +import { sandbox } from 'development-sandbox' +import { outdent } from 'outdent' -createNextDescribe( - 'Error overlay - RSC build errors', - { +describe('Error overlay - RSC build errors', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'rsc-build-errors')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - 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) - - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - '"getServerSideProps" is not supported in app/' - ) - - await cleanup() - }) - - it('should throw an error when metadata export is used in client components', async () => { - 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) - expect(await session.hasRedbox(true)).toBe(true) - 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) - expect(await session.hasRedbox(false)).toBe(false) - - // Add `generateMetadata` error - uncomment = content.replace( - '// export async function generateMetadata', - 'export async function generateMetadata' - ) - await session.patch(pageFile, uncomment) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - 'You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed.' - ) - - 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) - expect(await session.hasRedbox(true)).toBe(true) - 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) - - expect(await session.hasRedbox(true)).toBe(true) - 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' - ) - - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toInclude( - 'The default export is not a React Component in page:' - ) - - 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) - - expect(await session.hasRedbox(true)).toBe(true) - 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) - - expect(await session.hasRedbox(true)).toBe(true) - 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) - - expect(await session.hasRedbox(true)).toBe(true) - 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.` - ) - - await cleanup() - }) - - 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) - - expect(await session.hasRedbox(true)).toBe(true) - 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('() =>

hello dynamic world

', 'undefined') - ) - - expect(await session.hasRedbox(true)).toBe(true) - 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() {}' - ) - - expect(await session.hasRedbox(true)).toBe(true) - await check(() => session.getRedboxSource(), /must be a Client Component/) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + }) + + 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) + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + '"getServerSideProps" is not supported in app/' + ) + + await cleanup() + }) + + it('should throw an error when metadata export is used in client components', async () => { + 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) + expect(await session.hasRedbox(true)).toBe(true) + 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) + expect(await session.hasRedbox(false)).toBe(false) + + // Add `generateMetadata` error + uncomment = content.replace( + '// export async function generateMetadata', + 'export async function generateMetadata' + ) + await session.patch(pageFile, uncomment) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + 'You are attempting to export "generateMetadata" from a component marked with "use client", which is disallowed.' + ) + + 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) + expect(await session.hasRedbox(true)).toBe(true) + 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) + + expect(await session.hasRedbox(true)).toBe(true) + 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' + ) + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toInclude( + 'The default export is not a React Component in page:' + ) + + 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) + + expect(await session.hasRedbox(true)).toBe(true) + 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) + + expect(await session.hasRedbox(true)).toBe(true) + 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) + + expect(await session.hasRedbox(true)).toBe(true) + 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.` + ) + + await cleanup() + }) + + 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) + + expect(await session.hasRedbox(true)).toBe(true) + 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('() =>

hello dynamic world

', 'undefined') + ) + + expect(await session.hasRedbox(true)).toBe(true) + 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() {}' + ) + + expect(await session.hasRedbox(true)).toBe(true) + await check(() => session.getRedboxSource(), /must be a Client Component/) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./app/server-with-errors/error-file/error.js ReactServerComponentsError: @@ -291,47 +291,48 @@ createNextDescribe( Import path: ./app/server-with-errors/error-file/error.js" `) - ) + ) - await cleanup() - }) + await cleanup() + }) - 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' - ) + 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' + ) - // Empty file - await session.patch('app/server-with-errors/error-file/error.js', '') + // Empty file + await session.patch('app/server-with-errors/error-file/error.js', '') - expect(await session.hasRedbox(true)).toBe(true) - await check(() => session.getRedboxSource(), /must be a Client Component/) + expect(await session.hasRedbox(true)).toBe(true) + await check(() => session.getRedboxSource(), /must be a Client Component/) - // TODO: investigate flakey snapshot due to spacing below - // expect(next.normalizeTestDirContent(await session.getRedboxSource())) - // .toMatchInlineSnapshot(next.normalizeSnapshot(` - // "./app/server-with-errors/error-file/error.js - // ReactServerComponentsError: + // TODO: investigate flakey snapshot due to spacing below + // expect(next.normalizeTestDirContent(await session.getRedboxSource())) + // .toMatchInlineSnapshot(next.normalizeSnapshot(` + // "./app/server-with-errors/error-file/error.js + // ReactServerComponentsError: - // ./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. + // ./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. - // ,-[TEST_DIR/app/server-with-errors/error-file/error.js:1:1] - // 1 | - // : ^ - // \`---- + // ,-[TEST_DIR/app/server-with-errors/error-file/error.js:1:1] + // 1 | + // : ^ + // \`---- - // Import path: - // ./app/server-with-errors/error-file/error.js" - // `)) + // Import path: + // ./app/server-with-errors/error-file/error.js" + // `)) - await cleanup() - }) + await cleanup() + }) - it('should freeze parent resolved metadata to avoid mutating in generateMetadata', async () => { - const pagePath = 'app/metadata/mutate/page.js' - const content = `export default function page(props) { + 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) { return

mutate

} @@ -341,64 +342,66 @@ createNextDescribe( return { ...parentMetadata, } - }` - - const { session, cleanup } = await sandbox( - next, - undefined, - '/metadata/mutate' - ) - - await session.patch(pagePath, content) - - await check( - async () => ((await session.hasRedbox(true)) ? 'success' : 'fail'), - /success/ - ) - - expect(await session.getRedboxDescription()).toContain( - 'Cannot add property x, object is not extensible' - ) - - await cleanup() - }) - - it('should show which import caused an error in node_modules', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/client-package/module2.js', - "import { useState } from 'react'", - ], - ['node_modules/client-package/module1.js', "import './module2.js'"], - ['node_modules/client-package/index.js', "import './module1.js'"], - [ - 'node_modules/client-package/package.json', - ` - { - "name": "client-package", - "version": "0.0.1" - } - `, - ], - ['app/Component.js', "import 'client-package'"], - [ - 'app/page.js', - ` - import './Component.js' - export default function Page() { - return

Hello world

- }`, - ], - ]) - ) - - expect(await session.hasRedbox(true)).toBe(true) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + } + ` + + const { session, cleanup } = await sandbox( + next, + undefined, + '/metadata/mutate' + ) + + await session.patch(pagePath, content) + + await check( + async () => ((await session.hasRedbox(true)) ? 'success' : 'fail'), + /success/ + ) + + expect(await session.getRedboxDescription()).toContain( + 'Cannot add property x, object is not extensible' + ) + + await cleanup() + }) + + it('should show which import caused an error in node_modules', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/client-package/module2.js', + "import { useState } from 'react'", + ], + ['node_modules/client-package/module1.js', "import './module2.js'"], + ['node_modules/client-package/index.js', "import './module1.js'"], + [ + 'node_modules/client-package/package.json', + outdent` + { + "name": "client-package", + "version": "0.0.1" + } + `, + ], + ['app/Component.js', "import 'client-package'"], + [ + 'app/page.js', + outdent` + import './Component.js' + export default function Page() { + return

Hello world

+ } + `, + ], + ]) + ) + + expect(await session.hasRedbox(true)).toBe(true) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./app/Component.js ReactServerComponentsError: @@ -416,9 +419,8 @@ createNextDescribe( ./app/Component.js ./app/page.js" `) - ) + ) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance-app/server-components.test.ts b/test/development/acceptance-app/server-components.test.ts index ef71837e6527..9016043e9137 100644 --- a/test/development/acceptance-app/server-components.test.ts +++ b/test/development/acceptance-app/server-components.test.ts @@ -1,497 +1,505 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe, FileRef } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' import { check } from 'next-test-utils' +import { outdent } from 'outdent' -createNextDescribe( - 'Error Overlay for server components', - { +describe('Error Overlay for server components', () => { + const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - describe('createContext called in Server Component', () => { - it('should show error when React.createContext is called', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - import React from 'react' - const Context = React.createContext() - export default function Page() { - return ( - <> - -

Page

-
- - ) - }`, - ], - ]) - ) - - 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( + }) + + describe('createContext called in Server Component', () => { + it('should show error when React.createContext is called', async () => { + const { browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + import React from 'react' + const Context = React.createContext() + export default function Page() { + return ( + <> + +

Page

+
+ + ) + } + `, + ], + ]) + ) + + 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') - await cleanup() - }) - - it('should show error when React.createContext is called in external package', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const React = require('react') - module.exports = React.createContext() - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - [ - 'app/page.js', - ` - import Context from 'my-package' - export default function Page() { - return ( - <> - -

Page

-
- - ) - }`, - ], - ]) - ) + 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' + ) - 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' - ) + await cleanup() + }) - await cleanup() - }) - - it('should show error when createContext is called in external package', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const { createContext } = require('react') - module.exports = createContext() - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - [ - 'app/page.js', - ` - import Context from 'my-package' - export default function Page() { - return ( - <> - -

Page

-
- - ) - }`, - ], - ]) - ) - 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( + it('should show error when React.createContext is called in external package', async () => { + const { browser, cleanup } = await sandbox( + 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 ( + <> + +

Page

+
+ + ) + } + `, + ], + ]) + ) + + 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' + ) - await cleanup() - }) + await cleanup() }) - describe('React component hooks called in Server Component', () => { - it('should show error when React. is called', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - import React from 'react' - export default function Page() { - const ref = React.useRef() - return "Hello world" - }`, - ], - ]) + it('should show error when createContext is called in external package', async () => { + const { browser, cleanup } = await sandbox( + 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 ( + <> + +

Page

+
+ + ) + } + `, + ], + ]) + ) + 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' + ) - 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( + await cleanup() + }) + }) + + describe('React component hooks called in Server Component', () => { + it('should show error when React. is called', async () => { + const { browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + import React from 'react' + export default function Page() { + const ref = React.useRef() + return "Hello world" + } + `, + ], + ]) + ) + + 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') - await cleanup() - }) - - it('should show error when React. is called in external package', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const React = require('react') - module.exports = function Component() { - const [state, useState] = React.useState() - return "Hello world" - } - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - [ - 'app/page.js', - ` - import Component from 'my-package' - export default function Page() { - return - }`, - ], - ]) - ) + 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' + ) - 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' - ) + await cleanup() + }) - await cleanup() - }) - - it('should show error when React client hook is called in external package', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const { useEffect } = require('react') - module.exports = function Component() { - useEffect(() => {}, []) - return "Hello world" - } - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - [ - 'app/page.js', - ` - import Component from 'my-package' - export default function Page() { - return - }`, - ], - ]) + it('should show error when React. is called in external package', async () => { + const { browser, cleanup } = await sandbox( + 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 + } + `, + ], + ]) + ) + + 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') - 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' - ) + 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' + ) - await cleanup() - }) + await cleanup() }) - describe('Class component used in Server Component', () => { - it('should show error when Class Component is used', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - import React from 'react' - export default class Page extends React.Component { - render() { - return

Hello world

- } - } - `, - ], - ]) - ) - - 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 client hook is called in external package', async () => { + const { browser, cleanup } = await sandbox( + 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 + } + `, + ], + ]) + ) + + 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') - await cleanup() - }) - - it('should show error when React.PureComponent is rendered in external package', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const React = require('react') - module.exports = class extends React.PureComponent { - render() { - return "Hello world" - } - } - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - [ - 'app/page.js', - ` - import Component from 'my-package' - export default function Page() { - return - }`, - ], - ]) - ) + 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' + ) - 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( + await cleanup() + }) + }) + + describe('Class component used in Server Component', () => { + it('should show error when Class Component is used', async () => { + const { browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + import React from 'react' + export default class Page extends React.Component { + render() { + return

Hello world

+ } + } + `, + ], + ]) + ) + + 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') - await cleanup() - }) - - it('should show error when Component is rendered in external package', async () => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const { Component } = require('react') - module.exports = class extends Component { - render() { - return "Hello world" - } - } - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - [ - 'app/page.js', - ` - import Component from 'my-package' - export default function Page() { - return - }`, - ], - ]) - ) + expect(next.cliOutput).toContain( + 'This might be caused by a React Class Component being rendered in a Server Component' + ) + + await cleanup() + }) - 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( + it('should show error when React.PureComponent is rendered in external package', async () => { + const { browser, cleanup } = await sandbox( + 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 + } + `, + ], + ]) + ) + + 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' + ) - await cleanup() - }) + await cleanup() }) - describe('Next.js component hooks called in Server Component', () => { - it.each([ - // TODO-APP: add test for useParams - // ["useParams"], - ['useRouter'], - ['useSearchParams'], - ['useSelectedLayoutSegment'], - ['useSelectedLayoutSegments'], - ['usePathname'], - ])('should show error when %s is called', async (hook: string) => { - const { browser, cleanup } = await sandbox( - next, - new Map([ - [ - 'app/page.js', - ` - import { ${hook} } from 'next/navigation' - export default function Page() { - ${hook}() - return "Hello world" - }`, - ], - ]) + it('should show error when Component is rendered in external package', async () => { + const { browser, cleanup } = await sandbox( + 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 + } + `, + ], + ]) + ) + + 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' + ) - await check(async () => { - expect( - await browser - .waitForElementByCss('#nextjs__container_errors_desc') - .text() - ).toContain( - `Error: ${hook} 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( + await cleanup() + }) + }) + + describe('Next.js component hooks called in Server Component', () => { + it.each([ + // TODO-APP: add test for useParams + // ["useParams"], + ['useRouter'], + ['useSearchParams'], + ['useSelectedLayoutSegment'], + ['useSelectedLayoutSegments'], + ['usePathname'], + ])('should show error when %s is called', async (hook: string) => { + const { browser, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/page.js', + outdent` + import { ${hook} } from 'next/navigation' + export default function Page() { + ${hook}() + return "Hello world" + } + `, + ], + ]) + ) + + await check(async () => { + expect( + await browser + .waitForElementByCss('#nextjs__container_errors_desc') + .text() + ).toContain( `Error: ${hook} 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( + `Error: ${hook} 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` + ) - await cleanup() - }) + await cleanup() }) - } -) + }) +}) diff --git a/test/development/acceptance-app/version-staleness.test.ts b/test/development/acceptance-app/version-staleness.test.ts index bdb5eee31098..581529167271 100644 --- a/test/development/acceptance-app/version-staleness.test.ts +++ b/test/development/acceptance-app/version-staleness.test.ts @@ -1,120 +1,117 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe, FileRef } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import path from 'path' +import { outdent } from 'outdent' -describe.skip('should skip for now', () => { - createNextDescribe( - 'Error Overlay version staleness', - { - files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - dependencies: { - react: 'latest', - 'react-dom': 'latest', - }, - skipStart: true, +describe.skip('Error Overlay version staleness', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + dependencies: { + react: 'latest', + 'react-dom': 'latest', }, - ({ next }) => { - it('should show version staleness in runtime error', async () => { - // Set next to outdated version - const nextPackageJson = JSON.parse( - await next.readFile('node_modules/next/package.json') - ) - nextPackageJson.version = '1.0.0' - - const { browser, session, cleanup } = await sandbox( - next, - new Map([ - ['node_modules/next/package.json', JSON.stringify(nextPackageJson)], - ]) - ) - - await session.patch( - 'app/page.js', - ` + skipStart: true, + }) + + it('should show version staleness in runtime error', async () => { + // Set next to outdated version + const nextPackageJson = JSON.parse( + await next.readFile('node_modules/next/package.json') + ) + nextPackageJson.version = '1.0.0' + + const { browser, session, cleanup } = await sandbox( + next, + new Map([ + ['node_modules/next/package.json', JSON.stringify(nextPackageJson)], + ]) + ) + + await session.patch( + 'app/page.js', + outdent` "use client" - import Component from '../index' - export default function Page() { - setTimeout(() => { - throw new Error("runtime error") - }, 0) - return null - } + import Component from '../index' + export default function Page() { + setTimeout(() => { + throw new Error("runtime error") + }, 0) + return null + } ` - ) - - await session.waitForAndOpenRuntimeError() - expect( - await browser - .waitForElementByCss('.nextjs-container-build-error-version-status') - .text() - ).toMatchInlineSnapshot(`"Next.js (1.0.0) is outdated (learn more)"`) - - await cleanup() - }) - - it('should show version staleness in render error', async () => { - // Set next to outdated version - const nextPackageJson = JSON.parse( - await next.readFile('node_modules/next/package.json') - ) - nextPackageJson.version = '2.0.0' - - const { browser, session, cleanup } = await sandbox( - next, - new Map([ - ['node_modules/next/package.json', JSON.stringify(nextPackageJson)], - ]) - ) - - await session.patch( - 'app/page.js', - ` - export default function Page() { - throw new Error("render error") - return null - } + ) + + await session.waitForAndOpenRuntimeError() + expect( + await browser + .waitForElementByCss('.nextjs-container-build-error-version-status') + .text() + ).toMatchInlineSnapshot(`"Next.js (1.0.0) is outdated (learn more)"`) + + await cleanup() + }) + + it('should show version staleness in render error', async () => { + // Set next to outdated version + const nextPackageJson = JSON.parse( + await next.readFile('node_modules/next/package.json') + ) + nextPackageJson.version = '2.0.0' + + const { browser, session, cleanup } = await sandbox( + next, + new Map([ + ['node_modules/next/package.json', JSON.stringify(nextPackageJson)], + ]) + ) + + await session.patch( + 'app/page.js', + outdent` + export default function Page() { + throw new Error("render error") + return null + } ` - ) - - expect( - await browser - .waitForElementByCss('.nextjs-container-build-error-version-status') - .text() - ).toMatchInlineSnapshot(`"Next.js (2.0.0) is outdated (learn more)"`) - - await cleanup() - }) - - it('should show version staleness in build error', async () => { - // Set next to outdated version - const nextPackageJson = JSON.parse( - await next.readFile('node_modules/next/package.json') - ) - nextPackageJson.version = '3.0.0' - - const { browser, session, cleanup } = await sandbox( - next, - new Map([ - ['node_modules/next/package.json', JSON.stringify(nextPackageJson)], - ]) - ) - - await session.patch( - 'app/page.js', - ` + ) + + expect( + await browser + .waitForElementByCss('.nextjs-container-build-error-version-status') + .text() + ).toMatchInlineSnapshot(`"Next.js (2.0.0) is outdated (learn more)"`) + + await cleanup() + }) + + it('should show version staleness in build error', async () => { + // Set next to outdated version + const nextPackageJson = JSON.parse( + await next.readFile('node_modules/next/package.json') + ) + nextPackageJson.version = '3.0.0' + + const { browser, session, cleanup } = await sandbox( + next, + new Map([ + ['node_modules/next/package.json', JSON.stringify(nextPackageJson)], + ]) + ) + + await session.patch( + 'app/page.js', + outdent` {{{ ` - ) - - expect( - await browser - .waitForElementByCss('.nextjs-container-build-error-version-status') - .text() - ).toMatchInlineSnapshot(`"Next.js (3.0.0) is outdated (learn more)"`) - - await cleanup() - }) - } - ) + ) + + expect( + await browser + .waitForElementByCss('.nextjs-container-build-error-version-status') + .text() + ).toMatchInlineSnapshot(`"Next.js (3.0.0) is outdated (learn more)"`) + + await cleanup() + }) }) diff --git a/test/development/acceptance/ReactRefresh.test.ts b/test/development/acceptance/ReactRefresh.test.ts index 851d2aee86ca..b485edcdaa1f 100644 --- a/test/development/acceptance/ReactRefresh.test.ts +++ b/test/development/acceptance/ReactRefresh.test.ts @@ -1,25 +1,21 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import path from 'path' +import { outdent } from 'outdent' describe('ReactRefresh', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, }) - afterAll(() => next.destroy()) test('can edit a component without losing state', async () => { const { session, cleanup } = await sandbox(next) await session.patch( 'index.js', - ` + outdent` import { useCallback, useState } from 'react' export default function Index() { @@ -42,7 +38,7 @@ describe('ReactRefresh', () => { await session.patch( 'index.js', - ` + outdent` import { useCallback, useState } from 'react' export default function Index() { @@ -74,7 +70,7 @@ describe('ReactRefresh', () => { await session.write( 'NudgeOverview.js', - ` + outdent` import * as React from 'react'; import { foo } from './routes'; @@ -90,7 +86,7 @@ describe('ReactRefresh', () => { await session.write( 'SurveyOverview.js', - ` + outdent` const SurveyOverview = () => { return 100; }; @@ -101,7 +97,7 @@ describe('ReactRefresh', () => { await session.write( 'Milestones.js', - ` + outdent` import React from 'react'; import { fragment } from './DashboardPage'; @@ -117,7 +113,7 @@ describe('ReactRefresh', () => { await session.write( 'DashboardPage.js', - ` + outdent` import React from 'react'; import Milestones from './Milestones'; @@ -142,7 +138,7 @@ describe('ReactRefresh', () => { await session.write( 'routes.js', - ` + outdent` import DashboardPage from './DashboardPage'; export const foo = {}; @@ -156,7 +152,7 @@ describe('ReactRefresh', () => { await session.patch( 'index.js', - ` + outdent` import * as React from 'react'; import DashboardPage from './routes'; @@ -175,7 +171,7 @@ describe('ReactRefresh', () => { let didFullRefresh = !(await session.patch( 'SurveyOverview.js', - ` + outdent` const SurveyOverview = () => { return 200; }; @@ -191,7 +187,7 @@ describe('ReactRefresh', () => { didFullRefresh = !(await session.patch( 'index.js', - ` + outdent` import * as React from 'react'; import DashboardPage from './routes'; @@ -211,7 +207,7 @@ describe('ReactRefresh', () => { didFullRefresh = !(await session.patch( 'SurveyOverview.js', - ` + outdent` const SurveyOverview = () => { return 300; }; diff --git a/test/development/acceptance/ReactRefreshLogBox-app-doc.test.ts b/test/development/acceptance/ReactRefreshLogBox-app-doc.test.ts index 8421689ecfb5..163bdf84ebf4 100644 --- a/test/development/acceptance/ReactRefreshLogBox-app-doc.test.ts +++ b/test/development/acceptance/ReactRefreshLogBox-app-doc.test.ts @@ -1,70 +1,51 @@ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { getSnapshotTestDescribe } from 'next-test-utils' - -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`ReactRefreshLogBox ${variant}`, () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) - }) - afterAll(() => next.destroy()) - - test('empty _app shows logbox', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'pages/_app.js', - ` +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { describeVariants as describe } from 'next-test-utils' +import { outdent } from 'outdent' +import path from 'path' + +describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, + }) - `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot( - `"Error: The default export is not a React Component in page: \\"/_app\\""` - ) - - await session.patch( - 'pages/_app.js', - ` + test('empty _app shows logbox', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([['pages/_app.js', ``]]) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: \\"/_app\\""` + ) + + await session.patch( + 'pages/_app.js', + outdent` function MyApp({ Component, pageProps }) { return ; } export default MyApp ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) - - test('empty _document shows logbox', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'pages/_document.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await cleanup() + }) - `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot( - `"Error: The default export is not a React Component in page: \\"/_document\\""` - ) - - await session.patch( - 'pages/_document.js', - ` + test('empty _document shows logbox', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([['pages/_document.js', ``]]) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: \\"/_document\\""` + ) + + await session.patch( + 'pages/_document.js', + outdent` import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDocument extends Document { @@ -88,80 +69,76 @@ for (const variant of ['default', 'turbo']) { export default MyDocument ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) - - test('_app syntax error shows logbox', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'pages/_app.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await cleanup() + }) + + test('_app syntax error shows logbox', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'pages/_app.js', + outdent` function MyApp({ Component, pageProps }) { return <; } export default MyApp `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + ], + ]) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./pages/_app.js Error: x Expression expected ,-[TEST_DIR/pages/_app.js:1:1] - 1 | - 2 | function MyApp({ Component, pageProps }) { - 3 | return <; - : ^ - 4 | } - 5 | export default MyApp - 6 | + 1 | function MyApp({ Component, pageProps }) { + 2 | return <; + : ^ + 3 | } + 4 | export default MyApp \`---- x Expression expected ,-[TEST_DIR/pages/_app.js:1:1] - 1 | - 2 | function MyApp({ Component, pageProps }) { - 3 | return <; - : ^^^^^^^^^ - 4 | } - 5 | export default MyApp - 6 | + 1 | function MyApp({ Component, pageProps }) { + 2 | return <; + : ^^^^^^^^^ + 3 | } + 4 | export default MyApp \`---- Caused by: Syntax Error" `) - ) + ) - await session.patch( - 'pages/_app.js', - ` + await session.patch( + 'pages/_app.js', + outdent` function MyApp({ Component, pageProps }) { return ; } export default MyApp ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) - - test('_document syntax error shows logbox', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'pages/_document.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await cleanup() + }) + + test('_document syntax error shows logbox', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'pages/_document.js', + outdent` import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDocument extends Document {{ @@ -185,36 +162,35 @@ for (const variant of ['default', 'turbo']) { export default MyDocument `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + ], + ]) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./pages/_document.js Error: x Unexpected token \`{\`. Expected identifier, string literal, numeric literal or [ for the computed key ,-[TEST_DIR/pages/_document.js:1:1] - 1 | - 2 | import Document, { Html, Head, Main, NextScript } from 'next/document' - 3 | - 4 | class MyDocument extends Document {{ - : ^ - 5 | static async getInitialProps(ctx) { - 6 | const initialProps = await Document.getInitialProps(ctx) - 7 | return { ...initialProps } + 1 | import Document, { Html, Head, Main, NextScript } from 'next/document' + 2 | + 3 | class MyDocument extends Document {{ + : ^ + 4 | static async getInitialProps(ctx) { + 5 | const initialProps = await Document.getInitialProps(ctx) + 6 | return { ...initialProps } \`---- Caused by: Syntax Error" `) - ) + ) - await session.patch( - 'pages/_document.js', - ` + await session.patch( + 'pages/_document.js', + outdent` import Document, { Html, Head, Main, NextScript } from 'next/document' class MyDocument extends Document { @@ -238,9 +214,8 @@ for (const variant of ['default', 'turbo']) { export default MyDocument ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) + ) + expect(await session.hasRedbox(false)).toBe(false) + await cleanup() }) -} +}) diff --git a/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts b/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts index f3df273fae0f..36d51f018f87 100644 --- a/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts +++ b/test/development/acceptance/ReactRefreshLogBox-builtins.test.ts @@ -1,151 +1,149 @@ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { getSnapshotTestDescribe } from 'next-test-utils' - -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`ReactRefreshLogBox ${variant}`, () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) - }) - afterAll(() => next.destroy()) - - // Module trace is only available with webpack 5 - test('Node.js builtins', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'node_modules/my-package/index.js', - ` - const dns = require('dns') - module.exports = dns - `, - ], - [ - 'node_modules/my-package/package.json', - ` - { - "name": "my-package", - "version": "0.0.1" - } - `, - ], - ]) - ) - - await session.patch( - 'index.js', - ` - import pkg from 'my-package' - - export default function Hello() { - return (pkg ?

Package loaded

:

Package did not load

) - } - ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() - - await cleanup() - }) - - test('Module not found', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - `import Comp from 'b' - export default function Oops() { - return ( -
- lol -
- ) - } - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - - const source = await session.getRedboxSource() - expect(source).toMatchSnapshot() - - await cleanup() - }) - - test('Module not found (empty import trace)', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'pages/index.js', - `import Comp from 'b' - export default function Oops() { - return ( -
- lol -
- ) - } - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - - const source = await session.getRedboxSource() - expect(source).toMatchSnapshot() - - await cleanup() - }) - - test('Module not found (missing global CSS)', async () => { - const { session, cleanup } = await sandbox( - next, - new Map([ - [ - 'pages/_app.js', - ` - import './non-existent.css' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { describeVariants as describe } from 'next-test-utils' +import { outdent } from 'outdent' +import path from 'path' + +describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, + }) + + // Module trace is only available with webpack 5 + test('Node.js builtins', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'node_modules/my-package/index.js', + outdent` + const dns = require('dns') + module.exports = dns + `, + ], + [ + 'node_modules/my-package/package.json', + outdent` + { + "name": "my-package", + "version": "0.0.1" + } + `, + ], + ]) + ) + + await session.patch( + 'index.js', + outdent` + import pkg from 'my-package' + + export default function Hello() { + return (pkg ?

Package loaded

:

Package did not load

) + } + ` + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() + + await cleanup() + }) + + test('Module not found', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` + import Comp from 'b' + + export default function Oops() { + return ( +
+ lol +
+ ) + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source).toMatchSnapshot() + await cleanup() + }) + + test('Module not found (empty import trace)', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'pages/index.js', + outdent` + import Comp from 'b' + + export default function Oops() { + return ( +
+ lol +
+ ) + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source).toMatchSnapshot() + + await cleanup() + }) + + test('Module not found (missing global CSS)', async () => { + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'pages/_app.js', + outdent` + import './non-existent.css' + + export default function App({ Component, pageProps }) { + return + } + `, + ], + [ + 'pages/index.js', + outdent` + export default function Page(props) { + return

index page

+ } + `, + ], + ]) + ) + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source).toMatchSnapshot() + + await session.patch( + 'pages/_app.js', + outdent` export default function App({ Component, pageProps }) { return } - `, - ], - [ - 'pages/index.js', - ` - export default function Page(props) { - return

index page

- } - `, - ], - ]) - ) - expect(await session.hasRedbox(true)).toBe(true) - - const source = await session.getRedboxSource() - expect(source).toMatchSnapshot() - - await session.patch( - 'pages/_app.js', - ` - export default function App({ Component, pageProps }) { - return - } - ` - ) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.documentElement.innerHTML) - ).toContain('index page') - - await cleanup() - }) + ` + ) + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.documentElement.innerHTML) + ).toContain('index page') + + await cleanup() }) -} +}) diff --git a/test/development/acceptance/ReactRefreshLogBox-scss.test.ts b/test/development/acceptance/ReactRefreshLogBox-scss.test.ts index 2c83053ea5ca..33a51c6bbf27 100644 --- a/test/development/acceptance/ReactRefreshLogBox-scss.test.ts +++ b/test/development/acceptance/ReactRefreshLogBox-scss.test.ts @@ -1,23 +1,18 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import path from 'path' // TODO: figure out why snapshots mismatch on GitHub actions // specifically but work in docker and locally describe.skip('ReactRefreshLogBox', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - dependencies: { - sass: 'latest', - }, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, + dependencies: { + sass: 'latest', + }, }) - afterAll(() => next.destroy()) test('scss syntax errors', async () => { const { session, cleanup } = await sandbox(next) diff --git a/test/development/acceptance/ReactRefreshLogBox.test.ts b/test/development/acceptance/ReactRefreshLogBox.test.ts index 444824c258b9..2b1290edaed0 100644 --- a/test/development/acceptance/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance/ReactRefreshLogBox.test.ts @@ -1,27 +1,22 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { getSnapshotTestDescribe } from 'next-test-utils' - -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`ReactRefreshLogBox ${variant}`, () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) - }) - afterAll(() => next.destroy()) - - test('should strip whitespace correctly with newline', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { describeVariants as describe } from 'next-test-utils' +import path from 'path' +import { outdent } from 'outdent' + +describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, + }) + + test('should strip whitespace correctly with newline', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` export default function Page() { return ( <> @@ -37,24 +32,24 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - await session.evaluate(() => document.querySelector('a').click()) + ) + await session.evaluate(() => document.querySelector('a').click()) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807 - test('module init error not shown', async () => { - // Start here: - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137807 + test('module init error not shown', async () => { + // Start here: + const { session, cleanup } = await sandbox(next) - // We start here. - await session.patch( - 'index.js', - ` + // We start here. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { render() { @@ -63,16 +58,16 @@ for (const variant of ['default', 'turbo']) { } export default ClassDefault; ` - ) + ) - expect( - await session.evaluate(() => document.querySelector('h1').textContent) - ).toBe('Default Export') + expect( + await session.evaluate(() => document.querySelector('h1').textContent) + ).toBe('Default Export') - // Add a throw in module init phase: - await session.patch( - 'index.js', - ` + // Add a throw in module init phase: + await session.patch( + 'index.js', + outdent` // top offset for snapshot import * as React from 'react'; throw new Error('no') @@ -83,29 +78,29 @@ for (const variant of ['default', 'turbo']) { } export default ClassDefault; ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127 - test('boundaries', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554152127 + test('boundaries', async () => { + const { session, cleanup } = await sandbox(next) - await session.write( - 'FunctionDefault.js', - ` + await session.write( + 'FunctionDefault.js', + outdent` export default function FunctionDefault() { return

hello

} ` - ) - await session.patch( - 'index.js', - ` + ) + await session.patch( + 'index.js', + outdent` import FunctionDefault from './FunctionDefault.js' import * as React from 'react' class ErrorBoundary extends React.Component { @@ -135,58 +130,58 @@ for (const variant of ['default', 'turbo']) { } export default App; ` - ) + ) - expect( - await session.evaluate(() => document.querySelector('h2').textContent) - ).toBe('hello') + expect( + await session.evaluate(() => document.querySelector('h2').textContent) + ).toBe('hello') - await session.write( - 'FunctionDefault.js', - `export default function FunctionDefault() { throw new Error('no'); }` - ) + await session.write( + 'FunctionDefault.js', + `export default function FunctionDefault() { throw new Error('no'); }` + ) + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() + expect( + await session.evaluate(() => document.querySelector('h2').textContent) + ).toBe('error') + + await cleanup() + }) + + // TODO: investigate why this fails when running outside of the Next.js + // monorepo e.g. fails when using yarn create next-app + // https://github.com/vercel/next.js/pull/23203 + test.skip('internal package errors', async () => { + const { session, cleanup } = await sandbox(next) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() - expect( - await session.evaluate(() => document.querySelector('h2').textContent) - ).toBe('error') - - await cleanup() - }) - - // TODO: investigate why this fails when running outside of the Next.js - // monorepo e.g. fails when using yarn create next-app - // https://github.com/vercel/next.js/pull/23203 - test.skip('internal package errors', async () => { - const { session, cleanup } = await sandbox(next) - - // Make a react build-time error. - await session.patch( - 'index.js', - ` + // Make a react build-time error. + await session.patch( + 'index.js', + outdent` export default function FunctionNamed() { return
{{}}
}` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - // We internally only check the script path, not including the line number - // and error message because the error comes from an external library. - // This test ensures that the errored script path is correctly resolved. - expect(await session.getRedboxSource()).toContain( - `../../../../packages/next/dist/pages/_document.js` - ) + expect(await session.hasRedbox(true)).toBe(true) + // We internally only check the script path, not including the line number + // and error message because the error comes from an external library. + // This test ensures that the errored script path is correctly resolved. + expect(await session.getRedboxSource()).toContain( + `../../../../packages/next/dist/pages/_document.js` + ) - await cleanup() - }) + await cleanup() + }) - test('unterminated JSX', async () => { - const { session, cleanup } = await sandbox(next) + test('unterminated JSX', async () => { + const { session, cleanup } = await sandbox(next) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` export default () => { return (
@@ -195,13 +190,13 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` export default () => { return (
@@ -210,31 +205,30 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) + expect(await session.hasRedbox(true)).toBe(true) - const source = await session.getRedboxSource() - expect(next.normalizeTestDirContent(source)).toMatchInlineSnapshot( - next.normalizeSnapshot(` + const source = await session.getRedboxSource() + expect(next.normalizeTestDirContent(source)).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./index.js Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? - ,-[TEST_DIR/index.js:5:1] - 5 |

lol

- 6 | div - 7 | ) - 8 | } - : ^ - 9 | + ,-[TEST_DIR/index.js:4:1] + 4 |

lol

+ 5 | div + 6 | ) + 7 | } + : ^ \`---- x Unexpected eof - ,-[TEST_DIR/index.js:6:1] - 6 | div - 7 | ) - 8 | } - 9 | + ,-[TEST_DIR/index.js:4:1] + 4 |

lol

+ 5 | div + 6 | ) + 7 | } \`---- Caused by: @@ -244,27 +238,27 @@ for (const variant of ['default', 'turbo']) { ./index.js ./pages/index.js" `) - ) + ) - await cleanup() - }) + await cleanup() + }) - // Module trace is only available with webpack 5 - test('conversion to class component (1)', async () => { - const { session, cleanup } = await sandbox(next) + // Module trace is only available with webpack 5 + test('conversion to class component (1)', async () => { + const { session, cleanup } = await sandbox(next) - await session.write( - 'Child.js', - ` + await session.write( + 'Child.js', + outdent` export default function ClickCount() { return

hello

} ` - ) + ) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import Child from './Child'; export default function Home() { @@ -275,16 +269,16 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('hello') + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('hello') - await session.patch( - 'Child.js', - ` + await session.patch( + 'Child.js', + outdent` import { Component } from 'react'; export default class ClickCount extends Component { render() { @@ -292,14 +286,14 @@ for (const variant of ['default', 'turbo']) { } } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchSnapshot() + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchSnapshot() - await session.patch( - 'Child.js', - ` + await session.patch( + 'Child.js', + outdent` import { Component } from 'react'; export default class ClickCount extends Component { render() { @@ -307,23 +301,23 @@ for (const variant of ['default', 'turbo']) { } } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('hello new') + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('hello new') - await cleanup() - }) + await cleanup() + }) - test('css syntax errors', async () => { - const { session, cleanup } = await sandbox(next) + test('css syntax errors', async () => { + const { session, cleanup } = await sandbox(next) - await session.write('index.module.css', `.button {}`) - await session.patch( - 'index.js', - ` + await session.write('index.module.css', `.button {}`) + await session.patch( + 'index.js', + outdent` import './index.module.css'; export default () => { return ( @@ -333,35 +327,35 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) + + expect(await session.hasRedbox(false)).toBe(false) + + // Syntax error + await session.patch('index.module.css', `.button {`) + expect(await session.hasRedbox(true)).toBe(true) + const source = await session.getRedboxSource() + expect(source).toMatch('./index.module.css:1:1') + expect(source).toMatch('Syntax error: ') + expect(source).toMatch('Unclosed block') + expect(source).toMatch('> 1 | .button {') + expect(source).toMatch(' | ^') + + // Not local error + await session.patch('index.module.css', `button {}`) + expect(await session.hasRedbox(true)).toBe(true) + const source2 = await session.getRedboxSource() + expect(source2).toMatchSnapshot() + + await cleanup() + }) - expect(await session.hasRedbox(false)).toBe(false) - - // Syntax error - await session.patch('index.module.css', `.button {`) - expect(await session.hasRedbox(true)).toBe(true) - const source = await session.getRedboxSource() - expect(source).toMatch('./index.module.css:1:1') - expect(source).toMatch('Syntax error: ') - expect(source).toMatch('Unclosed block') - expect(source).toMatch('> 1 | .button {') - expect(source).toMatch(' | ^') - - // Not local error - await session.patch('index.module.css', `button {}`) - expect(await session.hasRedbox(true)).toBe(true) - const source2 = await session.getRedboxSource() - expect(source2).toMatchSnapshot() - - await cleanup() - }) - - test('logbox: anchors links in error messages', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` + test('logbox: anchors links in error messages', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -375,39 +369,39 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - expect(await session.hasRedbox(false)).toBe(false) - await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) - - const header = await session.getRedboxDescription() - expect(header).toMatchSnapshot() - expect( - await session.evaluate( - () => + ) + + expect(await session.hasRedbox(false)).toBe(false) + await session.evaluate(() => document.querySelector('button').click()) + expect(await session.hasRedbox(true)).toBe(true) + + const header = await session.getRedboxDescription() + expect(header).toMatchSnapshot() + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(1) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(1) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() - - await session.patch( - 'index.js', - ` + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() + + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -421,39 +415,39 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - expect(await session.hasRedbox(false)).toBe(false) - await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) - - const header2 = await session.getRedboxDescription() - expect(header2).toMatchSnapshot() - expect( - await session.evaluate( - () => + ) + + expect(await session.hasRedbox(false)).toBe(false) + await session.evaluate(() => document.querySelector('button').click()) + expect(await session.hasRedbox(true)).toBe(true) + + const header2 = await session.getRedboxDescription() + expect(header2).toMatchSnapshot() + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(1) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(1) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() - - await session.patch( - 'index.js', - ` + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() + + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -467,39 +461,39 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - expect(await session.hasRedbox(false)).toBe(false) - await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) - - const header3 = await session.getRedboxDescription() - expect(header3).toMatchSnapshot() - expect( - await session.evaluate( - () => + ) + + expect(await session.hasRedbox(false)).toBe(false) + await session.evaluate(() => document.querySelector('button').click()) + expect(await session.hasRedbox(true)).toBe(true) + + const header3 = await session.getRedboxDescription() + expect(header3).toMatchSnapshot() + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(1) + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(1) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() - - await session.patch( - 'index.js', - ` + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() + + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -513,53 +507,53 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - expect(await session.hasRedbox(false)).toBe(false) - await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) - - const header4 = await session.getRedboxDescription() - expect(header4).toMatchInlineSnapshot( - `"Error: multiple http://nextjs.org links http://example.com"` - ) - expect( - await session.evaluate( - () => + ) + + expect(await session.hasRedbox(false)).toBe(false) + await session.evaluate(() => document.querySelector('button').click()) + expect(await session.hasRedbox(true)).toBe(true) + + const header4 = await session.getRedboxDescription() + expect(header4).toMatchInlineSnapshot( + `"Error: multiple http://nextjs.org links http://example.com"` + ) + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(2) + expect( + await session.evaluate( + () => + ( + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() + expect( + await session.evaluate( + () => + ( document .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(2) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(2)' - ) as any - ).href - ) - ).toMatchSnapshot() - - await session.patch( - 'index.js', - ` + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(2)' + ) as any + ).href + ) + ).toMatchSnapshot() + + await session.patch( + 'index.js', + outdent` import { useCallback } from 'react' export default function Index() { @@ -573,59 +567,59 @@ for (const variant of ['default', 'turbo']) { ) } ` + ) + + expect(await session.hasRedbox(false)).toBe(false) + await session.evaluate(() => document.querySelector('button').click()) + expect(await session.hasRedbox(true)).toBe(true) + + const header5 = await session.getRedboxDescription() + expect(header5).toMatchInlineSnapshot( + `"Error: multiple http://nextjs.org links (http://example.com)"` + ) + expect( + await session.evaluate( + () => + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') + .length + ) + ).toBe(2) + expect( + await session.evaluate( + () => + ( + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(1)' + ) as any + ).href + ) + ).toMatchSnapshot() + expect( + await session.evaluate( + () => + ( + document + .querySelector('body > nextjs-portal') + .shadowRoot.querySelector( + '#nextjs__container_errors_desc a:nth-of-type(2)' + ) as any + ).href ) + ).toMatchSnapshot() - expect(await session.hasRedbox(false)).toBe(false) - await session.evaluate(() => document.querySelector('button').click()) - expect(await session.hasRedbox(true)).toBe(true) + await cleanup() + }) - const header5 = await session.getRedboxDescription() - expect(header5).toMatchInlineSnapshot( - `"Error: multiple http://nextjs.org links (http://example.com)"` - ) - expect( - await session.evaluate( - () => - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelectorAll('#nextjs__container_errors_desc a') - .length - ) - ).toBe(2) - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(1)' - ) as any - ).href - ) - ).toMatchSnapshot() - expect( - await session.evaluate( - () => - ( - document - .querySelector('body > nextjs-portal') - .shadowRoot.querySelector( - '#nextjs__container_errors_desc a:nth-of-type(2)' - ) as any - ).href - ) - ).toMatchSnapshot() - - await cleanup() - }) - - test('non-Error errors are handled properly', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` + test('non-Error errors are handled properly', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` export default () => { throw {'a': 1, 'b': 'x'}; return ( @@ -633,28 +627,28 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot( - `"Error: {\\"a\\":1,\\"b\\":\\"x\\"}"` - ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toMatchInlineSnapshot( + `"Error: {\\"a\\":1,\\"b\\":\\"x\\"}"` + ) - // fix previous error - await session.patch( - 'index.js', - ` + // fix previous error + await session.patch( + 'index.js', + outdent` export default () => { return (
hello
) } ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await session.patch( + 'index.js', + outdent` class Hello {} export default () => { @@ -664,27 +658,27 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toContain( - `Error: class Hello {` - ) - - // fix previous error - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toContain( + `Error: class Hello {` + ) + + // fix previous error + await session.patch( + 'index.js', + outdent` export default () => { return (
hello
) } ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await session.patch( + 'index.js', + outdent` export default () => { throw "string error" return ( @@ -692,27 +686,27 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot( - `"Error: string error"` - ) - - // fix previous error - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toMatchInlineSnapshot( + `"Error: string error"` + ) + + // fix previous error + await session.patch( + 'index.js', + outdent` export default () => { return (
hello
) } ` - ) - expect(await session.hasRedbox(false)).toBe(false) - await session.patch( - 'index.js', - ` + ) + expect(await session.hasRedbox(false)).toBe(false) + await session.patch( + 'index.js', + outdent` export default () => { throw null return ( @@ -720,13 +714,12 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxDescription()).toContain( - `Error: A null error was thrown` - ) + ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxDescription()).toContain( + `Error: A null error was thrown` + ) - await cleanup() - }) + await cleanup() }) -} +}) diff --git a/test/development/acceptance/ReactRefreshLogBoxMisc.test.ts b/test/development/acceptance/ReactRefreshLogBoxMisc.test.ts index 486fd8849852..557c2a221dc3 100644 --- a/test/development/acceptance/ReactRefreshLogBoxMisc.test.ts +++ b/test/development/acceptance/ReactRefreshLogBoxMisc.test.ts @@ -1,26 +1,22 @@ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import path from 'path' +import { outdent } from 'outdent' // TODO: re-enable these tests after figuring out what is causing // them to be so unreliable in CI describe.skip('ReactRefreshLogBox', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, }) - afterAll(() => next.destroy()) test(' with multiple children', async () => { const { session, cleanup } = await sandbox(next) await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Index() { @@ -59,7 +55,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -75,7 +71,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -87,7 +83,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -111,7 +107,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -135,7 +131,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -159,7 +155,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -184,7 +180,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'index.js', - ` + outdent` import Link from 'next/link' export default function Hello() { @@ -215,7 +211,7 @@ describe.skip('ReactRefreshLogBox', () => { await session.patch( 'pages/index.js', - ` + outdent` import myLibrary from 'my-non-existent-library' export async function getStaticProps() { return { diff --git a/test/development/acceptance/ReactRefreshModule.test.ts b/test/development/acceptance/ReactRefreshModule.test.ts index 58321126cb6f..6ef5a2d506c7 100644 --- a/test/development/acceptance/ReactRefreshModule.test.ts +++ b/test/development/acceptance/ReactRefreshModule.test.ts @@ -1,20 +1,15 @@ -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { sandbox } from './helpers' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import path from 'path' describe('ReactRefreshModule', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, }) - afterAll(() => next.destroy()) it('should allow any variable names', async () => { - const { session, cleanup } = await sandbox(next, new Map([])) + const { session, cleanup } = await sandbox(next) expect(await session.hasRedbox(false)).toBe(false) const variables = [ diff --git a/test/development/acceptance/ReactRefreshRegression.test.ts b/test/development/acceptance/ReactRefreshRegression.test.ts index 35def651e0b6..9f295a0c320f 100644 --- a/test/development/acceptance/ReactRefreshRegression.test.ts +++ b/test/development/acceptance/ReactRefreshRegression.test.ts @@ -1,62 +1,59 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' +import { outdent } from 'outdent' +import path from 'path' describe('ReactRefreshRegression', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - dependencies: { - 'styled-components': '5.1.0', - '@next/mdx': 'canary', - '@mdx-js/loader': '0.18.0', - }, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, + dependencies: { + 'styled-components': '5.1.0', + '@next/mdx': 'canary', + '@mdx-js/loader': '0.18.0', + }, }) - afterAll(() => next.destroy()) // https://github.com/vercel/next.js/issues/12422 test('styled-components hydration mismatch', async () => { - const files = new Map() - files.set( - 'pages/_document.js', - ` - import Document from 'next/document' - import { ServerStyleSheet } from 'styled-components' - - export default class MyDocument extends Document { - static async getInitialProps(ctx) { - const sheet = new ServerStyleSheet() - const originalRenderPage = ctx.renderPage - - try { - ctx.renderPage = () => - originalRenderPage({ - enhanceApp: App => props => sheet.collectStyles(), - }) - - const initialProps = await Document.getInitialProps(ctx) - return { - ...initialProps, - styles: ( - <> - {initialProps.styles} - {sheet.getStyleElement()} - - ), + const files = new Map([ + [ + 'pages/_document.js', + outdent` + import Document from 'next/document' + import { ServerStyleSheet } from 'styled-components' + + export default class MyDocument extends Document { + static async getInitialProps(ctx) { + const sheet = new ServerStyleSheet() + const originalRenderPage = ctx.renderPage + + try { + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: App => props => sheet.collectStyles(), + }) + + const initialProps = await Document.getInitialProps(ctx) + return { + ...initialProps, + styles: ( + <> + {initialProps.styles} + {sheet.getStyleElement()} + + ), + } + } finally { + sheet.seal() } - } finally { - sheet.seal() } } - } - ` - ) + `, + ], + ]) const { session, cleanup } = await sandbox(next, files) @@ -299,28 +296,31 @@ describe('ReactRefreshRegression', () => { // https://github.com/vercel/next.js/issues/13574 test('custom loader (mdx) should have Fast Refresh enabled', async () => { - const files = new Map() - files.set( - 'next.config.js', - ` - const withMDX = require("@next/mdx")({ - extension: /\\.mdx?$/, - }); - module.exports = withMDX({ - pageExtensions: ["js", "mdx"], - }); - ` + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'next.config.js', + outdent` + const withMDX = require("@next/mdx")({ + extension: /\\.mdx?$/, + }); + module.exports = withMDX({ + pageExtensions: ["js", "mdx"], + }); + `, + ], + ['pages/mdx.mdx', `Hello World!`], + ]), + '/mdx' ) - files.set('pages/index.mdx', `Hello World!`) - - const { session, cleanup } = await sandbox(next, files, false) expect( await session.evaluate( () => document.querySelector('#__next').textContent ) ).toBe('Hello World!') - let didNotReload = await session.patch('pages/index.mdx', `Hello Foo!`) + let didNotReload = await session.patch('pages/mdx.mdx', `Hello Foo!`) expect(didNotReload).toBe(true) expect(await session.hasRedbox(false)).toBe(false) expect( @@ -329,7 +329,7 @@ describe('ReactRefreshRegression', () => { ) ).toBe('Hello Foo!') - didNotReload = await session.patch('pages/index.mdx', `Hello Bar!`) + didNotReload = await session.patch('pages/mdx.mdx', `Hello Bar!`) expect(didNotReload).toBe(true) expect(await session.hasRedbox(false)).toBe(false) expect( diff --git a/test/development/acceptance/ReactRefreshRequire.test.ts b/test/development/acceptance/ReactRefreshRequire.test.ts index 284ea91dfd35..71daf614841b 100644 --- a/test/development/acceptance/ReactRefreshRequire.test.ts +++ b/test/development/acceptance/ReactRefreshRequire.test.ts @@ -1,18 +1,13 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import path from 'path' describe('ReactRefreshRequire', () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, }) - afterAll(() => next.destroy()) // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L989-L1048 test('re-runs accepted modules', async () => { diff --git a/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap b/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap index 61b51fea3087..d22b4a1d2f67 100644 --- a/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap +++ b/test/development/acceptance/__snapshots__/ReactRefreshLogBox-builtins.test.ts.snap @@ -4,22 +4,20 @@ exports[`ReactRefreshLogBox default Module not found (empty import trace) 1`] = "./pages/index.js:1:0 Module not found: Can't resolve 'b' > 1 | import Comp from 'b' - 2 | export default function Oops() { - 3 | return ( - 4 |
+ 2 | + 3 | export default function Oops() { + 4 | return ( https://nextjs.org/docs/messages/module-not-found" `; exports[`ReactRefreshLogBox default Module not found (missing global CSS) 1`] = ` -"./pages/_app.js:2:8 +"./pages/_app.js:1:0 Module not found: Can't resolve './non-existent.css' - 1 | -> 2 | import './non-existent.css' - | ^ - 3 | - 4 | export default function App({ Component, pageProps }) { - 5 | return +> 1 | import './non-existent.css' + 2 | + 3 | export default function App({ Component, pageProps }) { + 4 | return https://nextjs.org/docs/messages/module-not-found" `; @@ -28,9 +26,9 @@ exports[`ReactRefreshLogBox default Module not found 1`] = ` "./index.js:1:0 Module not found: Can't resolve 'b' > 1 | import Comp from 'b' - 2 | export default function Oops() { - 3 | return ( - 4 |
+ 2 | + 3 | export default function Oops() { + 4 | return ( https://nextjs.org/docs/messages/module-not-found @@ -39,7 +37,7 @@ Import trace for requested module: `; exports[`ReactRefreshLogBox default Node.js builtins 1`] = ` -"./node_modules/my-package/index.js:2:0 +"./node_modules/my-package/index.js:1:0 Module not found: Can't resolve 'dns' https://nextjs.org/docs/messages/module-not-found diff --git a/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap index 8a0214330075..f603c7365380 100644 --- a/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -8,15 +8,14 @@ exports[`ReactRefreshLogBox default boundaries 1`] = ` `; exports[`ReactRefreshLogBox default conversion to class component (1) 1`] = ` -"Child.js (5:18) @ ClickCount.render - - 3 | export default class ClickCount extends Component { - 4 | render() { -> 5 | throw new Error() - | ^ - 6 | } - 7 | } - 8 | " +"Child.js (4:10) @ ClickCount.render + + 2 | export default class ClickCount extends Component { + 3 | render() { +> 4 | throw new Error() + | ^ + 5 | } + 6 | }" `; exports[`ReactRefreshLogBox default css syntax errors 1`] = ` @@ -48,25 +47,25 @@ exports[`ReactRefreshLogBox default logbox: anchors links in error messages 11`] exports[`ReactRefreshLogBox default logbox: anchors links in error messages 12`] = `"http://example.com/"`; exports[`ReactRefreshLogBox default module init error not shown 1`] = ` -"index.js (4:14) @ eval - - 2 | // top offset for snapshot - 3 | import * as React from 'react'; -> 4 | throw new Error('no') - | ^ - 5 | class ClassDefault extends React.Component { - 6 | render() { - 7 | return

Default Export

;" +"index.js (3:6) @ eval + + 1 | // top offset for snapshot + 2 | import * as React from 'react'; +> 3 | throw new Error('no') + | ^ + 4 | class ClassDefault extends React.Component { + 5 | render() { + 6 | return

Default Export

;" `; exports[`ReactRefreshLogBox default should strip whitespace correctly with newline 1`] = ` -"index.js (9:34) @ onClick +"index.js (8:26) @ onClick - 7 | - 8 | { -> 9 | throw new Error('idk') + 6 | + 7 | { +> 8 | throw new Error('idk') | ^ - 10 | }}> - 11 | click me - 12 | " + 9 | }}> + 10 | click me + 11 | " `; diff --git a/test/development/acceptance/__snapshots__/error-recovery.test.ts.snap b/test/development/acceptance/__snapshots__/error-recovery.test.ts.snap index 69a69c4eb82f..373d7f774d7a 100644 --- a/test/development/acceptance/__snapshots__/error-recovery.test.ts.snap +++ b/test/development/acceptance/__snapshots__/error-recovery.test.ts.snap @@ -1,13 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReactRefreshLogBox default syntax > runtime error 1`] = ` -"index.js (6:16) @ Error +"index.js (5:8) @ Error - 4 | setInterval(() => { - 5 | i++ -> 6 | throw Error('no ' + i) - | ^ - 7 | }, 1000) - 8 | export default function FunctionNamed() { - 9 | return
" + 3 | setInterval(() => { + 4 | i++ +> 5 | throw Error('no ' + i) + | ^ + 6 | }, 1000) + 7 | export default function FunctionNamed() { + 8 | return
" `; diff --git a/test/development/acceptance/component-stack.test.ts b/test/development/acceptance/component-stack.test.ts index 8120ab08709e..ac229c08610e 100644 --- a/test/development/acceptance/component-stack.test.ts +++ b/test/development/acceptance/component-stack.test.ts @@ -1,54 +1,55 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { outdent } from 'outdent' +import path from 'path' -createNextDescribe( - 'Component Stack in error overlay', - { - files: {}, +describe('Component Stack in error overlay', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should show a component stack on hydration error', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'component.js', - ` - const isClient = typeof window !== 'undefined' - export default function Component() { - return ( -
-

{isClient ? "client" : "server"}

-
- ); - } -`, - ], - [ - 'index.js', - ` - import Component from './component' - export default function Mismatch() { - return ( -
- -
- ); - } -`, - ], - ]) - ) + }) - expect(await session.hasRedbox(true)).toBe(true) + it('should show a component stack on hydration error', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'component.js', + outdent` + const isClient = typeof window !== 'undefined' + export default function Component() { + return ( +
+

{isClient ? "client" : "server"}

+
+ ); + } + `, + ], + [ + 'index.js', + outdent` + import Component from './component' + export default function Mismatch() { + return ( +
+ +
+ ); + } + `, + ], + ]) + ) - expect(await session.getRedboxComponentStack()).toMatchInlineSnapshot(` + expect(await session.hasRedbox(true)).toBe(true) + + expect(await session.getRedboxComponentStack()).toMatchInlineSnapshot(` "p div Component @@ -56,7 +57,6 @@ createNextDescribe( Mismatch" `) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance/error-recovery.test.ts b/test/development/acceptance/error-recovery.test.ts index afda139c6bbc..b1f7f78b03bc 100644 --- a/test/development/acceptance/error-recovery.test.ts +++ b/test/development/acceptance/error-recovery.test.ts @@ -1,27 +1,22 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { check, getSnapshotTestDescribe } from 'next-test-utils' - -for (const variant of ['default', 'turbo']) { - getSnapshotTestDescribe(variant)(`ReactRefreshLogBox ${variant}`, () => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) - }) - afterAll(() => next.destroy()) - - test('logbox: can recover from a syntax error without losing state', async () => { - const { session, cleanup } = await sandbox(next) - - await session.patch( - 'index.js', - ` +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { check, describeVariants as describe } from 'next-test-utils' +import { outdent } from 'outdent' +import path from 'path' + +describe.each(['default', 'turbo'])('ReactRefreshLogBox %s', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), + skipStart: true, + }) + + test('logbox: can recover from a syntax error without losing state', async () => { + const { session, cleanup } = await sandbox(next) + + await session.patch( + 'index.js', + outdent` import { useCallback, useState } from 'react' export default function Index() { @@ -35,23 +30,23 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - await session.evaluate(() => document.querySelector('button').click()) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('1') + await session.evaluate(() => document.querySelector('button').click()) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('1') - await session.patch('index.js', `export default () =>
session.evaluate(() => document.querySelector('p').textContent), - /Count: 1/ - ) + await check( + () => session.evaluate(() => document.querySelector('p').textContent), + /Count: 1/ + ) - expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) + await cleanup() + }) - test('logbox: can recover from a event handler error', async () => { - const { session, cleanup } = await sandbox(next) + test('logbox: can recover from a event handler error', async () => { + const { session, cleanup } = await sandbox(next) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import { useCallback, useState } from 'react' export default function Index() { @@ -99,32 +94,32 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) - - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('0') - await session.evaluate(() => document.querySelector('button').click()) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('1') - - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toMatchInlineSnapshot(` - "index.js (8:18) @ eval - - 6 | const increment = useCallback(() => { - 7 | setCount(c => c + 1) - > 8 | throw new Error('oops') - | ^ - 9 | }, [setCount]) - 10 | return ( - 11 |
" - `) - - await session.patch( - 'index.js', - ` + ) + + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('0') + await session.evaluate(() => document.querySelector('button').click()) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('1') + + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toMatchInlineSnapshot(` + "index.js (7:10) @ eval + + 5 | const increment = useCallback(() => { + 6 | setCount(c => c + 1) + > 7 | throw new Error('oops') + | ^ + 8 | }, [setCount]) + 9 | return ( + 10 |
" + `) + + await session.patch( + 'index.js', + outdent` import { useCallback, useState } from 'react' export default function Index() { @@ -138,38 +133,38 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('Count: 1') - await session.evaluate(() => document.querySelector('button').click()) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('Count: 2') + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Count: 1') + await session.evaluate(() => document.querySelector('button').click()) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Count: 2') - expect(await session.hasRedbox(false)).toBe(false) + expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) + await cleanup() + }) - test('logbox: can recover from a component error', async () => { - const { session, cleanup } = await sandbox(next) + test('logbox: can recover from a component error', async () => { + const { session, cleanup } = await sandbox(next) - await session.write( - 'child.js', - ` + await session.write( + 'child.js', + outdent` export default function Child() { return

Hello

; } ` - ) + ) - await session.patch( - 'index.js', - ` + await session.patch( + 'index.js', + outdent` import Child from './child' export default function Index() { @@ -180,53 +175,53 @@ for (const variant of ['default', 'turbo']) { ) } ` - ) + ) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('Hello') + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Hello') - await session.patch( - 'child.js', - ` + await session.patch( + 'child.js', + outdent` // hello export default function Child() { throw new Error('oops') } ` - ) + ) - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - 'export default function Child()' - ) + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + 'export default function Child()' + ) - const didNotReload = await session.patch( - 'child.js', - ` + const didNotReload = await session.patch( + 'child.js', + outdent` export default function Child() { return

Hello

; } ` - ) + ) - expect(didNotReload).toBe(true) - expect(await session.hasRedbox(false)).toBe(false) - expect( - await session.evaluate(() => document.querySelector('p').textContent) - ).toBe('Hello') + expect(didNotReload).toBe(true) + expect(await session.hasRedbox(false)).toBe(false) + expect( + await session.evaluate(() => document.querySelector('p').textContent) + ).toBe('Hello') - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262 - test('render error not shown right after syntax error', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554137262 + test('render error not shown right after syntax error', async () => { + const { session, cleanup } = await sandbox(next) - // Starting here: - await session.patch( - 'index.js', - ` + // Starting here: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { render() { @@ -236,16 +231,16 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) + ) - expect( - await session.evaluate(() => document.querySelector('h1').textContent) - ).toBe('Default Export') + expect( + await session.evaluate(() => document.querySelector('h1').textContent) + ).toBe('Default Export') - // Break it with a syntax error: - await session.patch( - 'index.js', - ` + // Break it with a syntax error: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { @@ -256,13 +251,13 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) - expect(await session.hasRedbox(true)).toBe(true) + ) + expect(await session.hasRedbox(true)).toBe(true) - // Now change the code to introduce a runtime error without fixing the syntax error: - await session.patch( - 'index.js', - ` + // Now change the code to introduce a runtime error without fixing the syntax error: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { @@ -274,13 +269,13 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) - expect(await session.hasRedbox(true)).toBe(true) + ) + expect(await session.hasRedbox(true)).toBe(true) - // Now fix the syntax error: - await session.patch( - 'index.js', - ` + // Now fix the syntax error: + await session.patch( + 'index.js', + outdent` import * as React from 'react'; class ClassDefault extends React.Component { @@ -292,29 +287,29 @@ for (const variant of ['default', 'turbo']) { export default ClassDefault; ` - ) - expect(await session.hasRedbox(true)).toBe(true) + ) + expect(await session.hasRedbox(true)).toBe(true) - await check(async () => { - const source = await session.getRedboxSource() - return source?.includes('render() {') ? 'success' : source - }, 'success') + await check(async () => { + const source = await session.getRedboxSource() + return source?.includes('render() {') ? 'success' : source + }, 'success') - expect(await session.getRedboxSource()).toInclude( - "throw new Error('nooo');" - ) + expect(await session.getRedboxSource()).toInclude( + "throw new Error('nooo');" + ) - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016 - test('stuck error', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554144016 + test('stuck error', async () => { + const { session, cleanup } = await sandbox(next) - // We start here. - await session.patch( - 'index.js', - ` + // We start here. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; function FunctionDefault() { @@ -323,23 +318,23 @@ for (const variant of ['default', 'turbo']) { export default FunctionDefault; ` - ) + ) - // We add a new file. Let's call it Foo.js. - await session.write( - 'Foo.js', - ` + // We add a new file. Let's call it Foo.js. + await session.write( + 'Foo.js', + outdent` // intentionally skips export export default function Foo() { return React.createElement('h1', null, 'Foo'); } ` - ) + ) - // We edit our first file to use it. - await session.patch( - 'index.js', - ` + // We edit our first file to use it. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; import Foo from './Foo'; function FunctionDefault() { @@ -347,50 +342,50 @@ for (const variant of ['default', 'turbo']) { } export default FunctionDefault; ` - ) - - // We get an error because Foo didn't import React. Fair. - expect(await session.hasRedbox(true)).toBe(true) - expect(await session.getRedboxSource()).toInclude( - "return React.createElement('h1', null, 'Foo');" - ) - - // Let's add that to Foo. - await session.patch( - 'Foo.js', - ` + ) + + // We get an error because Foo didn't import React. Fair. + expect(await session.hasRedbox(true)).toBe(true) + expect(await session.getRedboxSource()).toInclude( + "return React.createElement('h1', null, 'Foo');" + ) + + // Let's add that to Foo. + await session.patch( + 'Foo.js', + outdent` import * as React from 'react'; export default function Foo() { return React.createElement('h1', null, 'Foo'); } ` - ) + ) - // Expected: this fixes the problem - expect(await session.hasRedbox(false)).toBe(false) + // Expected: this fixes the problem + expect(await session.hasRedbox(false)).toBe(false) - await cleanup() - }) + await cleanup() + }) - // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098 - test('syntax > runtime error', async () => { - const { session, cleanup } = await sandbox(next) + // https://github.com/pmmmwh/react-refresh-webpack-plugin/pull/3#issuecomment-554150098 + test('syntax > runtime error', async () => { + const { session, cleanup } = await sandbox(next) - // Start here. - await session.patch( - 'index.js', - ` + // Start here. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; export default function FunctionNamed() { return
} ` - ) - // TODO: this acts weird without above step - await session.patch( - 'index.js', - ` + ) + // TODO: this acts weird without above step + await session.patch( + 'index.js', + outdent` import * as React from 'react'; let i = 0 setInterval(() => { @@ -401,20 +396,20 @@ for (const variant of ['default', 'turbo']) { return
} ` - ) - - await new Promise((resolve) => setTimeout(resolve, 1000)) - expect(await session.hasRedbox(true)).toBe(true) - if (process.platform === 'win32') { - expect(await session.getRedboxSource()).toMatchSnapshot() - } else { - expect(await session.getRedboxSource()).toMatchSnapshot() - } - - // Make a syntax error. - await session.patch( - 'index.js', - ` + ) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(await session.hasRedbox(true)).toBe(true) + if (process.platform === 'win32') { + expect(await session.getRedboxSource()).toMatchSnapshot() + } else { + expect(await session.getRedboxSource()).toMatchSnapshot() + } + + // Make a syntax error. + await session.patch( + 'index.js', + outdent` import * as React from 'react'; let i = 0 setInterval(() => { @@ -422,23 +417,23 @@ for (const variant of ['default', 'turbo']) { throw Error('no ' + i) }, 1000) export default function FunctionNamed() {` - ) - - await new Promise((resolve) => setTimeout(resolve, 1000)) - expect(await session.hasRedbox(true)).toBe(true) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + ) + + await new Promise((resolve) => setTimeout(resolve, 1000)) + expect(await session.hasRedbox(true)).toBe(true) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./index.js Error: x Expected '}', got '' - ,-[TEST_DIR/index.js:5:1] - 5 | i++ - 6 | throw Error('no ' + i) - 7 | }, 1000) - 8 | export default function FunctionNamed() { - : ^ + ,-[TEST_DIR/index.js:4:1] + 4 | i++ + 5 | throw Error('no ' + i) + 6 | }, 1000) + 7 | export default function FunctionNamed() { + : ^ \`---- Caused by: @@ -448,24 +443,24 @@ for (const variant of ['default', 'turbo']) { ./index.js ./pages/index.js" `) - ) - - // Test that runtime error does not take over: - await new Promise((resolve) => setTimeout(resolve, 2000)) - expect(await session.hasRedbox(true)).toBe(true) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + ) + + // Test that runtime error does not take over: + await new Promise((resolve) => setTimeout(resolve, 2000)) + expect(await session.hasRedbox(true)).toBe(true) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./index.js Error: x Expected '}', got '' - ,-[TEST_DIR/index.js:5:1] - 5 | i++ - 6 | throw Error('no ' + i) - 7 | }, 1000) - 8 | export default function FunctionNamed() { - : ^ + ,-[TEST_DIR/index.js:4:1] + 4 | i++ + 5 | throw Error('no ' + i) + 6 | }, 1000) + 7 | export default function FunctionNamed() { + : ^ \`---- Caused by: @@ -475,9 +470,8 @@ for (const variant of ['default', 'turbo']) { ./index.js ./pages/index.js" `) - ) + ) - await cleanup() - }) + await cleanup() }) -} +}) diff --git a/test/development/acceptance/fixtures/default-template/index.js b/test/development/acceptance/fixtures/default-template/index.js new file mode 100644 index 000000000000..31fd86d55937 --- /dev/null +++ b/test/development/acceptance/fixtures/default-template/index.js @@ -0,0 +1 @@ +export default () => 'new sandbox' diff --git a/test/development/acceptance/fixtures/default-template/next.config.js b/test/development/acceptance/fixtures/default-template/next.config.js new file mode 100644 index 000000000000..4ba52ba2c8df --- /dev/null +++ b/test/development/acceptance/fixtures/default-template/next.config.js @@ -0,0 +1 @@ +module.exports = {} diff --git a/test/development/acceptance/fixtures/default-template/pages/index.js b/test/development/acceptance/fixtures/default-template/pages/index.js new file mode 100644 index 000000000000..88aa8f478820 --- /dev/null +++ b/test/development/acceptance/fixtures/default-template/pages/index.js @@ -0,0 +1 @@ +export { default } from '../index' diff --git a/test/development/acceptance/helpers.ts b/test/development/acceptance/helpers.ts deleted file mode 100644 index c76a506c1201..000000000000 --- a/test/development/acceptance/helpers.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { - getRedboxDescription, - getRedboxHeader, - getRedboxSource, - hasRedbox, -} from 'next-test-utils' -import webdriver from 'next-webdriver' -import { NextInstance } from 'test/lib/next-modes/base' - -export async function sandbox( - next: NextInstance, - initialFiles?: Map, - defaultFiles = true -) { - await next.stop() - await next.clean() - - if (defaultFiles) { - await next.patchFile( - 'pages/index.js', - `export { default } from '../index';` - ) - await next.patchFile('index.js', `export default () => 'new sandbox';`) - } - - if (initialFiles) { - for (const [k, v] of initialFiles.entries()) { - await next.patchFile(k, v) - } - } - await next.start() - const browser = await webdriver(next.url, '/') - return { - session: { - async write(filename, content) { - // Update the file on filesystem - await next.patchFile(filename, content) - }, - async patch(filename, content) { - // Register an event for HMR completion - await browser.eval(function () { - ;(window as any).__HMR_STATE = 'pending' - - var timeout = setTimeout(() => { - ;(window as any).__HMR_STATE = 'timeout' - }, 30 * 1000) - ;(window as any).__NEXT_HMR_CB = function () { - clearTimeout(timeout) - ;(window as any).__HMR_STATE = 'success' - } - }) - - await this.write(filename, content) - - for (;;) { - const status = await browser.eval(() => (window as any).__HMR_STATE) - if (!status) { - await new Promise((resolve) => setTimeout(resolve, 750)) - - // Wait for application to re-hydrate: - await browser.evalAsync(function () { - var callback = arguments[arguments.length - 1] - if ((window as any).__NEXT_HYDRATED) { - callback() - } else { - var timeout = setTimeout(callback, 30 * 1000) - ;(window as any).__NEXT_HYDRATED_CB = function () { - clearTimeout(timeout) - callback() - } - } - }) - - console.log('Application re-loaded.') - // Slow down tests a bit: - await new Promise((resolve) => setTimeout(resolve, 750)) - return false - } - if (status === 'success') { - console.log('Hot update complete.') - break - } - if (status !== 'pending') { - throw new Error(`Application is in inconsistent state: ${status}.`) - } - - await new Promise((resolve) => setTimeout(resolve, 30)) - } - - // Slow down tests a bit (we don't know how long re-rendering takes): - await new Promise((resolve) => setTimeout(resolve, 750)) - return true - }, - async remove(filename) { - await next.deleteFile(filename) - }, - async evaluate(snippet: () => any) { - if (typeof snippet === 'function') { - const result = await browser.eval(snippet) - await new Promise((resolve) => setTimeout(resolve, 30)) - return result - } else { - throw new Error( - `You must pass a function to be evaluated in the browser.` - ) - } - }, - async hasRedbox(expected = false) { - return hasRedbox(browser, expected) - }, - async getRedboxDescription() { - return getRedboxDescription(browser) - }, - async getRedboxSource(includeHeader = false) { - const header = includeHeader ? await getRedboxHeader(browser) : '' - const source = await getRedboxSource(browser) - - if (includeHeader) { - return `${header}\n\n${source}` - } - return source - }, - async getRedboxComponentStack() { - await browser.waitForElementByCss('[data-nextjs-component-stack-frame]') - const componentStackFrameElements = await browser.elementsByCss( - '[data-nextjs-component-stack-frame]' - ) - const componentStackFrameTexts = await Promise.all( - componentStackFrameElements.map((f) => f.innerText()) - ) - - return componentStackFrameTexts.join('\n') - }, - }, - async cleanup() { - await browser.close() - await next.stop() - await next.clean() - }, - } -} diff --git a/test/development/acceptance/hydration-error.test.ts b/test/development/acceptance/hydration-error.test.ts index 3f67fa0bd3b8..449ecef63064 100644 --- a/test/development/acceptance/hydration-error.test.ts +++ b/test/development/acceptance/hydration-error.test.ts @@ -1,41 +1,42 @@ /* eslint-env jest */ -import { sandbox } from './helpers' -import { createNextDescribe } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { outdent } from 'outdent' +import path from 'path' -createNextDescribe( - 'Error overlay for hydration errors', - { - files: {}, +describe('Error overlay for hydration errors', () => { + const { next } = nextTestSetup({ + files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - it('should show correct hydration error when client and server render different text', async () => { - const { cleanup, session } = await sandbox( - next, - new Map([ - [ - 'index.js', - ` - const isClient = typeof window !== 'undefined' - export default function Mismatch() { - return ( -
-
{isClient ? "client" : "server"}
-
- ); - } -`, - ], - ]) - ) + }) - expect(await session.hasRedbox(true)).toBe(true) + it('should show correct hydration error when client and server render different text', async () => { + const { cleanup, session } = await sandbox( + next, + new Map([ + [ + 'index.js', + outdent` + const isClient = typeof window !== 'undefined' + export default function Mismatch() { + return ( +
+
{isClient ? "client" : "server"}
+
+ ); + } + `, + ], + ]) + ) - expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` + expect(await session.hasRedbox(true)).toBe(true) + + expect(await session.getRedboxDescription()).toMatchInlineSnapshot(` "Error: Text content does not match server-rendered HTML. Warning: Text content did not match. Server: \\"server\\" Client: \\"client\\" @@ -43,7 +44,6 @@ createNextDescribe( See more info here: https://nextjs.org/docs/messages/react-hydration-error" `) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts index c0c1373a1c8b..2f3f3b34d5f3 100644 --- a/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts +++ b/test/development/acceptance/server-component-compiler-errors-in-pages.test.ts @@ -1,124 +1,127 @@ /* eslint-env jest */ -import { createNextDescribe } from 'e2e-utils' +import { nextTestSetup } from 'e2e-utils' import { check } from 'next-test-utils' -import { sandbox } from './helpers' +import { sandbox } from 'development-sandbox' +import { outdent } from 'outdent' const initialFiles = new Map([ ['next.config.js', 'module.exports = { experimental: { appDir: true } }'], ['app/_.js', ''], // app dir need to exists, otherwise the SWC RSC checks will not run [ 'pages/index.js', - `import Comp from '../components/Comp' + outdent` + import Comp from '../components/Comp' - export default function Page() { return }`, + export default function Page() { return } + `, ], [ 'components/Comp.js', - `export default function Comp() { return

Hello world

}`, + outdent` + export default function Comp() { + return

Hello world

+ } + `, ], ]) -createNextDescribe( - 'Error Overlay for server components compiler errors in pages', - { +describe('Error Overlay for server components compiler errors in pages', () => { + const { next } = nextTestSetup({ files: {}, dependencies: { react: 'latest', 'react-dom': 'latest', }, skipStart: true, - }, - ({ next }) => { - test("importing 'next/headers' in pages", async () => { - const { session, cleanup } = await sandbox(next, initialFiles, false) - - await session.patch( - 'components/Comp.js', - ` + }) + + test("importing 'next/headers' in pages", async () => { + const { session, cleanup } = await sandbox(next, initialFiles) + + await session.patch( + 'components/Comp.js', + outdent` import { cookies } from 'next/headers' - + export default function Page() { return

hello world

} - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - await check( - () => session.getRedboxSource(), - /That only works in a Server Component/ - ) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + await check( + () => session.getRedboxSource(), + /That only works in a Server Component/ + ) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./components/Comp.js ReactServerComponentsError: You're importing a component that needs next/headers. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/react-essentials#server-components ,-[TEST_DIR/components/Comp.js:1:1] - 1 | - 2 | import { cookies } from 'next/headers' - : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - 3 | - 4 | export default function Page() { - 5 | return

hello world

+ 1 | import { cookies } from 'next/headers' + : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + 2 | + 3 | export default function Page() { + 4 | return

hello world

\`---- Import trace for requested module: ./components/Comp.js ./pages/index.js" `) - ) - - await cleanup() - }) - - test("importing 'server-only' in pages", async () => { - const { session, cleanup } = await sandbox(next, initialFiles, false) - - await next.patchFile( - 'components/Comp.js', - ` - import 'server-only' - - export default function Page() { - return 'hello world' - } - ` - ) - - expect(await session.hasRedbox(true)).toBe(true) - await check( - () => session.getRedboxSource(), - /That only works in a Server Component/ - ) - expect( - next.normalizeTestDirContent(await session.getRedboxSource()) - ).toMatchInlineSnapshot( - next.normalizeSnapshot(` + ) + + await cleanup() + }) + + test("importing 'server-only' in pages", async () => { + const { session, cleanup } = await sandbox(next, initialFiles) + + await next.patchFile( + 'components/Comp.js', + outdent` + import 'server-only' + + export default function Page() { + return 'hello world' + } + ` + ) + + expect(await session.hasRedbox(true)).toBe(true) + await check( + () => session.getRedboxSource(), + /That only works in a Server Component/ + ) + expect( + next.normalizeTestDirContent(await session.getRedboxSource()) + ).toMatchInlineSnapshot( + next.normalizeSnapshot(` "./components/Comp.js ReactServerComponentsError: You're importing a component that needs server-only. That only works in a Server Component which is not supported in the pages/ directory. Read more: https://nextjs.org/docs/getting-started/react-essentials#server-components ,-[TEST_DIR/components/Comp.js:1:1] - 1 | - 2 | import 'server-only' - : ^^^^^^^^^^^^^^^^^^^^ - 3 | - 4 | export default function Page() { - 5 | return 'hello world' + 1 | import 'server-only' + : ^^^^^^^^^^^^^^^^^^^^ + 2 | + 3 | export default function Page() { + 4 | return 'hello world' \`---- Import trace for requested module: ./components/Comp.js ./pages/index.js" `) - ) + ) - await cleanup() - }) - } -) + await cleanup() + }) +}) diff --git a/test/development/middleware-warnings/index.test.ts b/test/development/middleware-warnings/index.test.ts index fb72050354c3..a6121306c9dc 100644 --- a/test/development/middleware-warnings/index.test.ts +++ b/test/development/middleware-warnings/index.test.ts @@ -1,65 +1,74 @@ -import { createNext } from 'e2e-utils' -import { NextInstance } from 'test/lib/next-modes/base' -import { sandbox } from '../acceptance/helpers' +import { FileRef, nextTestSetup } from 'e2e-utils' +import { sandbox } from 'development-sandbox' +import path from 'path' +import { outdent } from 'outdent' const middlewarePath = 'middleware.js' const middlewareWarning = `A middleware can not alter response's body` describe('middlewares', () => { - let next: NextInstance - let cleanup - - beforeAll(async () => { - next = await createNext({ - files: {}, - skipStart: true, - }) + const { next } = nextTestSetup({ + files: new FileRef( + path.join(__dirname, '..', 'acceptance', 'fixtures', 'default-template') + ), + skipStart: true, }) - afterAll(() => next.destroy()) - + let cleanup afterEach(() => cleanup?.()) it.each([ { title: 'returning response with literal string', - code: `export default function middleware() { - return new Response('this is not allowed'); - }`, + code: outdent` + export default function middleware() { + return new Response('this is not allowed'); + } + `, }, { title: 'returning response with literal number', - code: `export default function middleware() { - return new Response(10); - }`, + code: outdent` + export default function middleware() { + return new Response(10); + } + `, }, { title: 'returning response with JSON.stringify', - code: `export default function middleware() { - return new Response(JSON.stringify({ foo: 'this is not allowed' })); - }`, + code: outdent` + export default function middleware() { + return new Response(JSON.stringify({ foo: 'this is not allowed' })); + } + `, }, { title: 'populating response with a value', - code: `export default function middleware(request) { - const body = JSON.stringify({ foo: 'this should not be allowed, but hard to detect with AST' }) - return new Response(body); - }`, + code: outdent` + export default function middleware(request) { + const body = JSON.stringify({ foo: 'this should not be allowed, but hard to detect with AST' }) + return new Response(body); + } + `, }, { title: 'populating response with a function call', - code: `function buildBody() { - return 'this should not be allowed, but hard to detect with AST' - } - export default function middleware(request) { - return new Response(buildBody()); - }`, + code: outdent` + function buildBody() { + return 'this should not be allowed, but hard to detect with AST' + } + export default function middleware(request) { + return new Response(buildBody()); + } + `, }, { title: 'populating response with an async function call', - code: `export default async function middleware(request) { - return new Response(await fetch('https://example.vercel.sh')); - }`, + code: outdent` + export default async function middleware(request) { + return new Response(await fetch('https://example.vercel.sh')); + } + `, }, ])('does not warn when $title', async ({ code }) => { ;({ cleanup } = await sandbox(next, new Map([[middlewarePath, code]]))) @@ -69,15 +78,19 @@ describe('middlewares', () => { it.each([ { title: 'returning null reponse body', - code: `export default function middleware() { - return new Response(null); - }`, + code: outdent` + export default function middleware() { + return new Response(null); + } + `, }, { title: 'returning undefined response body', - code: `export default function middleware() { - return new Response(undefined); - }`, + code: outdent` + export default function middleware() { + return new Response(undefined); + } + `, }, ])('does not warn when $title', async ({ code }) => { ;({ cleanup } = await sandbox(next, new Map([[middlewarePath, code]]))) diff --git a/test/development/acceptance-app/helpers.ts b/test/lib/development-sandbox.ts similarity index 78% rename from test/development/acceptance-app/helpers.ts rename to test/lib/development-sandbox.ts index 1d2df4ea9db0..f1dde37b38d4 100644 --- a/test/development/acceptance-app/helpers.ts +++ b/test/lib/development-sandbox.ts @@ -3,9 +3,26 @@ import { getRedboxHeader, getRedboxSource, hasRedbox, -} from 'next-test-utils' -import webdriver from 'next-webdriver' -import { NextInstance } from 'test/lib/next-modes/base' + waitFor, +} from './next-test-utils' +import webdriver from './next-webdriver' +import { NextInstance } from './next-modes/base' +import { BrowserInterface } from './browsers/base' + +export function waitForHydration(browser: BrowserInterface): Promise { + return browser.evalAsync(function () { + var callback = arguments[arguments.length - 1] + if ((window as any).__NEXT_HYDRATED) { + callback() + } else { + var timeout = setTimeout(callback, 30 * 1000) + ;(window as any).__NEXT_HYDRATED_CB = function () { + clearTimeout(timeout) + callback() + } + } + }) +} export async function sandbox( next: NextInstance, @@ -21,8 +38,10 @@ export async function sandbox( await next.patchFile(k, v) } } + await next.start() const browser = await webdriver(next.url, initialUrl, webDriverOptions) + // await waitForHydration(browser) return { browser, session: { @@ -49,25 +68,14 @@ export async function sandbox( for (;;) { const status = await browser.eval(() => (window as any).__HMR_STATE) if (!status) { - await new Promise((resolve) => setTimeout(resolve, 750)) + await waitFor(750) // Wait for application to re-hydrate: - await browser.evalAsync(function () { - var callback = arguments[arguments.length - 1] - if ((window as any).__NEXT_HYDRATED) { - callback() - } else { - var timeout = setTimeout(callback, 30 * 1000) - ;(window as any).__NEXT_HYDRATED_CB = function () { - clearTimeout(timeout) - callback() - } - } - }) + await waitForHydration(browser) console.log('Application re-loaded.') // Slow down tests a bit: - await new Promise((resolve) => setTimeout(resolve, 750)) + await waitFor(750) return false } if (status === 'success') { @@ -78,11 +86,11 @@ export async function sandbox( throw new Error(`Application is in inconsistent state: ${status}.`) } - await new Promise((resolve) => setTimeout(resolve, 30)) + await waitFor(30) } // Slow down tests a bit (we don't know how long re-rendering takes): - await new Promise((resolve) => setTimeout(resolve, 750)) + await waitFor(750) return true }, async remove(filename) { @@ -91,7 +99,7 @@ export async function sandbox( async evaluate(snippet: () => any) { if (typeof snippet === 'function') { const result = await browser.eval(snippet) - await new Promise((resolve) => setTimeout(resolve, 30)) + await waitFor(30) return result } else { throw new Error( @@ -124,7 +132,10 @@ export async function sandbox( return source }, async getRedboxComponentStack() { - await browser.waitForElementByCss('[data-nextjs-component-stack-frame]') + await browser.waitForElementByCss( + '[data-nextjs-component-stack-frame]', + 30000 + ) const componentStackFrameElements = await browser.elementsByCss( '[data-nextjs-component-stack-frame]' ) diff --git a/test/lib/e2e-utils.ts b/test/lib/e2e-utils.ts index 0dd22afb0c62..1d739f6de28c 100644 --- a/test/lib/e2e-utils.ts +++ b/test/lib/e2e-utils.ts @@ -202,6 +202,73 @@ export async function createNext( } } +export function nextTestSetup( + options: Parameters[0] & { + skipDeployment?: boolean + dir?: string + } +): { + isNextDev: boolean + isNextDeploy: boolean + isNextStart: boolean + isTurbopack: boolean + next: NextInstance + skipped: boolean +} { + let skipped = false + + if (options.skipDeployment) { + // When the environment is running for deployment tests. + if ((global as any).isNextDeploy) { + // eslint-disable-next-line jest/no-focused-tests + it.only('should skip next deploy', () => {}) + // No tests are run. + skipped = true + } + } + + let next: NextInstance + if (!skipped) { + beforeAll(async () => { + next = await createNext(options) + }) + afterAll(async () => { + await next.destroy() + }) + } + + const nextProxy = new Proxy({} as NextInstance, { + get: function (_target, property) { + const prop = next[property] + return typeof prop === 'function' ? prop.bind(next) : prop + }, + }) + + return { + get isNextDev(): boolean { + return Boolean((global as any).isNextDev) + }, + get isTurbopack(): boolean { + return Boolean( + (global as any).isNextDev && + !process.env.TEST_WASM && + (options.turbo ?? shouldRunTurboDevTest()) + ) + }, + + get isNextDeploy(): boolean { + return Boolean((global as any).isNextDeploy) + }, + get isNextStart(): boolean { + return Boolean((global as any).isNextStart) + }, + get next() { + return nextProxy + }, + skipped, + } +} + export function createNextDescribe( name: string, options: Parameters[0] & { @@ -217,50 +284,12 @@ export function createNextDescribe( }) => void ): void { describe(name, () => { - if (options.skipDeployment) { - // When the environment is running for deployment tests. - if ((global as any).isNextDeploy) { - it('should skip next deploy', () => {}) - // No tests are run. - return - } - } + const context = nextTestSetup(options) - let next: NextInstance - beforeAll(async () => { - next = await createNext(options) - }) - afterAll(async () => { - await next.destroy() - }) + if (context.skipped) { + return + } - const nextProxy = new Proxy({} as NextInstance, { - get: function (_target, property) { - const prop = next[property] - return typeof prop === 'function' ? prop.bind(next) : prop - }, - }) - fn({ - get isNextDev(): boolean { - return Boolean((global as any).isNextDev) - }, - get isTurbopack(): boolean { - return Boolean( - (global as any).isNextDev && - !process.env.TEST_WASM && - (options.turbo ?? shouldRunTurboDevTest()) - ) - }, - - get isNextDeploy(): boolean { - return Boolean((global as any).isNextDeploy) - }, - get isNextStart(): boolean { - return Boolean((global as any).isNextStart) - }, - get next() { - return nextProxy - }, - }) + fn(context) }) } diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index 4bc8c11a8db1..e9d6cba01312 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -937,7 +937,9 @@ export function shouldRunTurboDevTest() { export function getSnapshotTestDescribe(variant) { const runningEnv = variant ?? 'default' if (runningEnv !== 'default' && runningEnv !== 'turbo') { - throw new Error(`Check if test env passed correctly ${variant}`) + throw new Error( + `An invalid test env was passed: ${variant} (only "default" and "turbo" are valid options)` + ) } const shouldRunTurboDev = shouldRunTurboDevTest() @@ -947,3 +949,25 @@ export function getSnapshotTestDescribe(variant) { return shouldSkip ? describe.skip : describe } + +/** + * For better editor support, pass in the variants this should run on (`default` and/or `turbo`) as cases. + * + * This is necessary if separate snapshots are needed for next.js with webpack vs turbopack. + * + * @type {Pick} + */ +export const describeVariants = { + each: (variants) => (name, fn) => { + if ( + !Array.isArray(variants) || + !variants.every((val) => typeof val === 'string') + ) { + throw new Error('variants need to be an array of strings') + } + + for (const variant of variants) { + getSnapshotTestDescribe(variant).each([variant])(name, fn) + } + }, +} diff --git a/tsconfig.json b/tsconfig.json index 5bf06b43dc3f..911b5d84d7cb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "baseUrl": ".", "types": ["react", "jest", "node", "trusted-types"], "paths": { + "development-sandbox": ["test/lib/development-sandbox"], "next-test-utils": ["test/lib/next-test-utils"], "amp-test-utils": ["test/lib/amp-test-utils"], "next-webdriver": ["test/lib/next-webdriver"],