+ )
+ }
+ `
+ )
+
+ await check(
+ () => session.evaluate(() => document.querySelector('p').textContent),
+ /Count: 1/
+ )
+
+ expect(await session.hasRedbox()).toBe(false)
+
+ await cleanup()
+ })
+
+ // TODO-APP: re-enable when error recovery doesn't reload the page.
+ test.skip('logbox: 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 (
+
+ )
+ }
+ `
+ )
+
+ 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)
+ if (process.platform === 'win32') {
+ expect(await session.getRedboxSource()).toMatchSnapshot()
+ } else {
+ expect(await session.getRedboxSource()).toMatchSnapshot()
+ }
+
+ 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 (
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).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.hasRedbox()).toBe(false)
+
+ await cleanup()
+ })
+
+ test('logbox: can recover from a component error', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.write(
+ 'child.js',
+ `
+ export default function Child() {
+ return
;
+ }
+ `
+ )
+
+ await session.patch(
+ 'index.js',
+ `
+ import Child from './child'
+
+ export default function Index() {
+ return (
+
+ )
+ }
+ `
+ )
+
+ expect(
+ await session.evaluate(() => document.querySelector('p').textContent)
+ ).toBe('Hello')
+
+ 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()).toMatchSnapshot()
+
+ // TODO-APP: re-enable when error recovery doesn't reload the page.
+ /* const didNotReload = */ await session.patch(
+ 'child.js',
+ `
+ export default function Child() {
+ return
;
+ }
+ `
+ )
+
+ // TODO-APP: re-enable when error recovery doesn't reload the page.
+ // expect(didNotReload).toBe(true)
+ expect(await session.hasRedbox()).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-554137262
+ test('render error not shown right after syntax error', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ // Starting here:
+ await session.patch(
+ 'index.js',
+ `
+ import * as React from 'react';
+ class ClassDefault extends React.Component {
+ render() {
+ return
;
+ }
+ }
+
+ export default ClassDefault;
+ `
+ )
+
+ expect(
+ await session.evaluate(() => document.querySelector('h1').textContent)
+ ).toBe('Default Export')
+
+ // Break it with a syntax error:
+ await session.patch(
+ 'index.js',
+ `
+ import * as React from 'react';
+
+ class ClassDefault extends React.Component {
+ render()
+ return
;
+ }
+ }
+
+ export default ClassDefault;
+ `
+ )
+ 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',
+ `
+ import * as React from 'react';
+
+ class ClassDefault extends React.Component {
+ render()
+ throw new Error('nooo');
+ return
;
+ }
+ }
+
+ export default ClassDefault;
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+
+ // Now fix the syntax error:
+ await session.patch(
+ 'index.js',
+ `
+ import * as React from 'react';
+
+ class ClassDefault extends React.Component {
+ render() {
+ throw new Error('nooo');
+ return
;
+ }
+ }
+
+ export default ClassDefault;
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxSource()).toMatchSnapshot()
+
+ 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)
+
+ // We start here.
+ await session.patch(
+ 'index.js',
+ `
+ import * as React from 'react';
+ class ClassDefault extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ export default ClassDefault;
+ `
+ )
+
+ expect(
+ await session.evaluate(() => document.querySelector('h1').textContent)
+ ).toBe('Default Export')
+
+ // Add a throw in module init phase:
+ await session.patch(
+ 'index.js',
+ `
+ // top offset for snapshot
+ import * as React from 'react';
+ throw new Error('no')
+ class ClassDefault extends React.Component {
+ render() {
+ return
;
+ }
+ }
+ 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()
+ }
+
+ await cleanup()
+ })
+
+ // 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',
+ `
+ import * as React from 'react';
+
+ function FunctionDefault() {
+ return
;
+ }
+
+ export default FunctionDefault;
+ `
+ )
+
+ // We add a new file. Let's call it Foo.js.
+ await session.write(
+ 'Foo.js',
+ `
+ // 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',
+ `
+ import * as React from 'react';
+ import Foo from './Foo';
+ function FunctionDefault() {
+ return
;
+ }
+ 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()).toMatchSnapshot()
+
+ // Let's add that to Foo.
+ await session.patch(
+ 'Foo.js',
+ `
+ import * as React from 'react';
+ export default function Foo() {
+ return React.createElement('h1', null, 'Foo');
+ }
+ `
+ )
+
+ // Expected: this fixes the problem
+ expect(await session.hasRedbox()).toBe(false)
+
+ 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',
+ `
+ import * as React from 'react';
+
+ export default function FunctionNamed() {
+ return
+ }
+ `
+ )
+ // TODO: this acts weird without above step
+ await session.patch(
+ 'index.js',
+ `
+ import * as React from 'react';
+ let i = 0
+ setInterval(() => {
+ i++
+ throw Error('no ' + i)
+ }, 1000)
+ export default function FunctionNamed() {
+ 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',
+ `
+ import * as React from 'react';
+ let i = 0
+ setInterval(() => {
+ i++
+ throw Error('no ' + i)
+ }, 1000)
+ export default function FunctionNamed() {`
+ )
+
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxSource()).toMatchSnapshot()
+
+ // 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()).toMatchSnapshot()
+
+ await cleanup()
+ })
+
+ // 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',
+ `
+ export default function FunctionDefault() {
+ return
+ }
+ `
+ )
+ await session.patch(
+ 'index.js',
+ `
+ import FunctionDefault from './FunctionDefault.js'
+ import * as React from 'react'
+ class ErrorBoundary extends React.Component {
+ constructor() {
+ super()
+ this.state = { hasError: false, error: null };
+ }
+ static getDerivedStateFromError(error) {
+ return {
+ hasError: true,
+ error
+ };
+ }
+ render() {
+ if (this.state.hasError) {
+ return this.props.fallback;
+ }
+ return this.props.children;
+ }
+ }
+ function App() {
+ return (
+
+ );
+ }
+ export default App;
+ `
+ )
+
+ expect(
+ await session.evaluate(() => document.querySelector('h2').textContent)
+ ).toBe('hello')
+
+ 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)
+
+ // Make a react build-time error.
+ await session.patch(
+ 'index.js',
+ `
+ 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`
+ )
+
+ await cleanup()
+ })
+
+ test('unterminated JSX', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ return (
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).toBe(false)
+
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ return (
+
+
lol
+ div
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox(true)).toBe(true)
+
+ const source = await session.getRedboxSource()
+ expect(source).toMatchSnapshot()
+
+ await cleanup()
+ })
+
+ // 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',
+ `
+ export default function ClickCount() {
+ return
hello
+ }
+ `
+ )
+
+ await session.patch(
+ 'index.js',
+ `
+ import Child from './Child';
+
+ export default function Home() {
+ return (
+
+
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).toBe(false)
+ expect(
+ await session.evaluate(() => document.querySelector('p').textContent)
+ ).toBe('hello')
+
+ await session.patch(
+ 'Child.js',
+ `
+ import { Component } from 'react';
+ export default class ClickCount extends Component {
+ render() {
+ throw new Error()
+ }
+ }
+ `
+ )
+
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxSource()).toMatchSnapshot()
+
+ await session.patch(
+ 'Child.js',
+ `
+ import { Component } from 'react';
+ export default class ClickCount extends Component {
+ render() {
+ return
hello new
+ }
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).toBe(false)
+ expect(
+ await session.evaluate(() => document.querySelector('p').textContent)
+ ).toBe('hello new')
+
+ await cleanup()
+ })
+
+ test('css syntax errors', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.write('index.module.css', `.button {}`)
+ await session.patch(
+ 'index.js',
+ `
+ import './index.module.css';
+ export default () => {
+ return (
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).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',
+ `
+ import { useCallback } from 'react'
+
+ export default function Index() {
+ const boom = useCallback(() => {
+ throw new Error('end http://nextjs.org')
+ }, [])
+ return (
+
+
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).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.querySelector(
+ '#nextjs__container_errors_desc a:nth-of-type(1)'
+ ) as any
+ ).href
+ )
+ ).toMatchSnapshot()
+
+ await session.patch(
+ 'index.js',
+ `
+ import { useCallback } from 'react'
+
+ export default function Index() {
+ const boom = useCallback(() => {
+ throw new Error('http://nextjs.org start')
+ }, [])
+ return (
+
+
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).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.querySelector(
+ '#nextjs__container_errors_desc a:nth-of-type(1)'
+ ) as any
+ ).href
+ )
+ ).toMatchSnapshot()
+
+ await session.patch(
+ 'index.js',
+ `
+ import { useCallback } from 'react'
+
+ export default function Index() {
+ const boom = useCallback(() => {
+ throw new Error('middle http://nextjs.org end')
+ }, [])
+ return (
+
+
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).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.querySelector(
+ '#nextjs__container_errors_desc a:nth-of-type(1)'
+ ) as any
+ ).href
+ )
+ ).toMatchSnapshot()
+
+ await session.patch(
+ 'index.js',
+ `
+ import { useCallback } from 'react'
+
+ export default function Index() {
+ const boom = useCallback(() => {
+ throw new Error('multiple http://nextjs.org links http://example.com')
+ }, [])
+ return (
+
+
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox()).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.querySelector(
+ '#nextjs__container_errors_desc a:nth-of-type(2)'
+ ) as any
+ ).href
+ )
+ ).toMatchSnapshot()
+
+ 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)
+
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ throw {'a': 1, 'b': 'x'};
+ return (
+
hello
+ )
+ }
+ `
+ )
+
+ 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',
+ `
+ export default () => {
+ return (
+
hello
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(false)).toBe(false)
+ await session.patch(
+ 'index.js',
+ `
+ class Hello {}
+
+ export default () => {
+ throw Hello
+ return (
+
hello
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toContain(
+ `Error: class Hello {`
+ )
+
+ // fix previous error
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ return (
+
hello
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(false)).toBe(false)
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ throw "string error"
+ return (
+
hello
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
+ `"Error: string error"`
+ )
+
+ // fix previous error
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ return (
+
hello
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(false)).toBe(false)
+ await session.patch(
+ 'index.js',
+ `
+ export default () => {
+ throw null
+ return (
+
hello
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toContain(
+ `Error: A null error was thrown`
+ )
+
+ await cleanup()
+ })
+})
diff --git a/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts b/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts
new file mode 100644
index 0000000000000..48822651cab52
--- /dev/null
+++ b/test/development/acceptance-app/ReactRefreshLogBoxMisc.test.ts
@@ -0,0 +1,242 @@
+import { sandbox } from './helpers'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import path from 'path'
+
+// TODO: re-enable these tests after figuring out what is causing
+// them to be so unreliable in CI
+describe.skip('ReactRefreshLogBox app', () => {
+ if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
+ it('should skip for react v17', () => {})
+ return
+ }
+
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ 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',
+ `
+ import Link from 'next/link'
+
+ export default function Index() {
+ return (
+
+
One
+
Two
+
+ )
+ }
+ `
+ )
+
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
+ `"Error: Multiple children were passed to
with \`href\` of \`/\` but only one child is supported https://nextjs.org/docs/messages/link-multiple-children"`
+ )
+ expect(
+ await session.evaluate(
+ () =>
+ (
+ document
+ .querySelector('body > nextjs-portal')
+ .shadowRoot.querySelector(
+ '#nextjs__container_errors_desc a:nth-of-type(1)'
+ ) as any
+ ).href
+ )
+ ).toMatch('https://nextjs.org/docs/messages/link-multiple-children')
+
+ await cleanup()
+ })
+
+ test('
component props errors', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return
+ }
+ `
+ )
+
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toMatchInlineSnapshot(
+ `"Error: Failed prop type: The prop \`href\` expects a \`string\` or \`object\` in \`
\`, but got \`undefined\` instead."`
+ )
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return
Abc
+ }
+ `
+ )
+ expect(await session.hasRedbox()).toBe(false)
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return (
+
+ Abc
+
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox()).toBe(false)
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return (
+
+ Abc
+
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox()).toBe(false)
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return (
+
+ Abc
+
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox()).toBe(false)
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return (
+
+ Abc
+
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toMatchSnapshot()
+
+ await session.patch(
+ 'index.js',
+ `
+ import Link from 'next/link'
+
+ export default function Hello() {
+ return (
+
+ Abc
+
+ )
+ }
+ `
+ )
+ expect(await session.hasRedbox(true)).toBe(true)
+ expect(await session.getRedboxDescription()).toMatchSnapshot()
+
+ await cleanup()
+ })
+
+ test('server-side only compilation errors', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'app/page.js',
+ `'use client'
+ import myLibrary from 'my-non-existent-library'
+ export async function getStaticProps() {
+ return {
+ props: {
+ result: myLibrary()
+ }
+ }
+ }
+ export default function Hello(props) {
+ return
{props.result}
+ }
+ `
+ )
+
+ expect(await session.hasRedbox(true)).toBe(true)
+ await cleanup()
+ })
+})
diff --git a/test/development/acceptance-app/ReactRefreshModule.test.ts b/test/development/acceptance-app/ReactRefreshModule.test.ts
new file mode 100644
index 0000000000000..ec76bf6b81787
--- /dev/null
+++ b/test/development/acceptance-app/ReactRefreshModule.test.ts
@@ -0,0 +1,51 @@
+import { createNext, FileRef } from 'e2e-utils'
+import path from 'path'
+import { NextInstance } from 'test/lib/next-modes/base'
+import { sandbox } from './helpers'
+
+describe('ReactRefreshModule app', () => {
+ if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
+ it('should skip for react v17', () => {})
+ return
+ }
+
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ 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([]))
+ expect(await session.hasRedbox()).toBe(false)
+
+ const variables = [
+ '_a',
+ '_b',
+ 'currentExports',
+ 'prevExports',
+ 'isNoLongerABoundary',
+ ]
+
+ 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
+ }`
+ )
+ expect(await session.hasRedbox()).toBe(false)
+ expect(next.cliOutput).not.toContain(
+ `'${variable}' has already been declared`
+ )
+ }
+
+ await cleanup()
+ })
+})
diff --git a/test/development/acceptance-app/ReactRefreshRegression.test.ts b/test/development/acceptance-app/ReactRefreshRegression.test.ts
new file mode 100644
index 0000000000000..3cb39a3a942d2
--- /dev/null
+++ b/test/development/acceptance-app/ReactRefreshRegression.test.ts
@@ -0,0 +1,349 @@
+/* eslint-env jest */
+import { sandbox } from './helpers'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import path from 'path'
+
+describe('ReactRefreshRegression app', () => {
+ if (process.env.NEXT_TEST_REACT_VERSION === '^17') {
+ it('should skip for react v17', () => {})
+ return
+ }
+
+ 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',
+ },
+ })
+ })
+ afterAll(() => next.destroy())
+
+ // https://github.com/vercel/next.js/issues/12422
+ // TODO-APP: port to app directory
+ test.skip('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()}
+ >
+ ),
+ }
+ } finally {
+ sheet.seal()
+ }
+ }
+ }
+ `
+ )
+
+ const { session, cleanup } = await sandbox(next, files)
+
+ // We start here.
+ await session.patch(
+ 'index.js',
+ `
+ import React from 'react'
+ import styled from 'styled-components'
+
+ const Title = styled.h1\`
+ color: red;
+ font-size: 50px;
+ \`
+
+ export default () =>
My page
+ `
+ )
+
+ // Verify no hydration mismatch:
+ expect(await session.hasRedbox()).toBe(false)
+
+ await cleanup()
+ })
+
+ // https://github.com/vercel/next.js/issues/13978
+ test('can fast refresh a page with static generation', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'app/page.js',
+ `'use client'
+ import { useCallback, useState } from 'react'
+
+ export default function Index() {
+ const [count, setCount] = useState(0)
+ const increment = useCallback(() => setCount(c => c + 1), [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.patch(
+ 'app/page.js',
+ `'use client'
+ 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}
+
+
+ )
+ }
+ `
+ )
+
+ 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 cleanup()
+ })
+
+ // https://github.com/vercel/next.js/issues/13978
+ // TODO-APP: fix case where server component is moved to a client component
+ test.skip('can fast refresh a page with dynamic rendering', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'app/page.js',
+ `
+ export const revalidate = 0
+
+ import Component from '../index'
+ export default function Page() {
+ return
+ }
+ `
+ )
+ await session.patch(
+ 'index.js',
+ `'use client'
+ import { useCallback, useState } from 'react'
+
+ export default function Index() {
+ const [count, setCount] = useState(0)
+ const increment = useCallback(() => setCount(c => c + 1), [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.patch(
+ 'index.js',
+ `'use client'
+ 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}
+
+
+ )
+ }
+ `
+ )
+
+ 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 cleanup()
+ })
+
+ // https://github.com/vercel/next.js/issues/13978
+ // TODO-APP: fix case where server component is moved to a client component
+ test.skip('can fast refresh a page with config', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'app/page.js',
+ `
+ export const config = {}
+
+ import Component from '../index'
+ export default function Page() {
+ return
+ }
+ `
+ )
+
+ await session.patch(
+ 'index.js',
+ `'use client'
+ import { useCallback, useState } from 'react'
+
+ export const config = {}
+
+ export default function Index() {
+ const [count, setCount] = useState(0)
+ const increment = useCallback(() => setCount(c => c + 1), [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 cleanup()
+ })
+
+ // https://github.com/vercel/next.js/issues/11504
+ // TODO-APP: fix case where error is not resolved to source correctly.
+ test.skip('shows an overlay for a server-side error', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'app/page.js',
+ `export default function () { throw new Error('pre boom'); }`
+ )
+
+ const didNotReload = await session.patch(
+ 'app/page.js',
+ `export default function () { throw new Error('boom'); }`
+ )
+ expect(didNotReload).toBe(false)
+
+ expect(await session.hasRedbox(true)).toBe(true)
+
+ const source = await session.getRedboxSource()
+ expect(source.split(/\r?\n/g).slice(2).join('\n')).toMatchInlineSnapshot(`
+ "> 1 | export default function () { throw new Error('boom'); }
+ | ^"
+ `)
+
+ await cleanup()
+ })
+
+ // 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"],
+ experimental: { appDir: true },
+ });
+ `
+ )
+ files.set('app/content.mdx', `Hello World!`)
+ files.set(
+ 'app/page.js',
+ `'use client'
+ import MDX from './content.mdx'
+ export default function Page() {
+ return
+ }
+ `
+ )
+
+ const { session, cleanup } = await sandbox(next, files)
+ expect(
+ await session.evaluate(
+ () => document.querySelector('#content').textContent
+ )
+ ).toBe('Hello World!')
+
+ let didNotReload = await session.patch('app/content.mdx', `Hello Foo!`)
+ expect(didNotReload).toBe(true)
+ expect(await session.hasRedbox()).toBe(false)
+ expect(
+ await session.evaluate(
+ () => document.querySelector('#content').textContent
+ )
+ ).toBe('Hello Foo!')
+
+ didNotReload = await session.patch('app/content.mdx', `Hello Bar!`)
+ expect(didNotReload).toBe(true)
+ expect(await session.hasRedbox()).toBe(false)
+ expect(
+ await session.evaluate(
+ () => document.querySelector('#content').textContent
+ )
+ ).toBe('Hello Bar!')
+
+ await cleanup()
+ })
+})
diff --git a/test/development/acceptance-app/ReactRefreshRequire.test.ts b/test/development/acceptance-app/ReactRefreshRequire.test.ts
new file mode 100644
index 0000000000000..7bbf3bf8ca1a1
--- /dev/null
+++ b/test/development/acceptance-app/ReactRefreshRequire.test.ts
@@ -0,0 +1,505 @@
+/* eslint-env jest */
+import { sandbox } from './helpers'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'test/lib/next-modes/base'
+import path from 'path'
+
+describe('ReactRefreshRequire app', () => {
+ let next: NextInstance
+
+ beforeAll(async () => {
+ next = await createNext({
+ 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 () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'index.js',
+ `export default function Noop() { return null; };`
+ )
+
+ await session.write(
+ './foo.js',
+ `window.log.push('init FooV1'); require('./bar');`
+ )
+ await session.write(
+ './bar.js',
+ `window.log.push('init BarV1'); export default function Bar() { return null; };`
+ )
+
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ 'index.js',
+ `require('./foo'); export default function Noop() { return null; };`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init FooV1',
+ 'init BarV1',
+ ])
+
+ // We only edited Bar, and it accepted.
+ // So we expect it to re-run alone.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ './bar.js',
+ `window.log.push('init BarV2'); export default function Bar() { return null; };`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV2',
+ ])
+
+ // We only edited Bar, and it accepted.
+ // So we expect it to re-run alone.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ './bar.js',
+ `window.log.push('init BarV3'); export default function Bar() { return null; };`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV3',
+ ])
+
+ // TODO:
+ // expect(Refresh.performReactRefresh).toHaveBeenCalled();
+ // expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
+
+ await cleanup()
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1050-L1137
+ test('propagates a hot update to closest accepted module', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'index.js',
+ `export default function Noop() { return null; };`
+ )
+
+ await session.write(
+ './foo.js',
+ `
+ window.log.push('init FooV1');
+ require('./bar');
+
+ // Exporting a component marks it as auto-accepting.
+ export default function Foo() {};
+ `
+ )
+
+ await session.write('./bar.js', `window.log.push('init BarV1');`)
+
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ 'index.js',
+ `require('./foo'); export default function Noop() { return null; };`
+ )
+
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init FooV1',
+ 'init BarV1',
+ ])
+
+ // We edited Bar, but it doesn't accept.
+ // So we expect it to re-run together with Foo which does.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch('./bar.js', `window.log.push('init BarV2');`)
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ // // FIXME: Metro order:
+ // 'init BarV2',
+ // 'init FooV1',
+ 'init FooV1',
+ 'init BarV2',
+ // Webpack runs in this order because it evaluates modules parent down, not
+ // child up. Parents will re-run child modules in the order that they're
+ // imported from the parent.
+ ])
+
+ // We edited Bar, but it doesn't accept.
+ // So we expect it to re-run together with Foo which does.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch('./bar.js', `window.log.push('init BarV3');`)
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ // // FIXME: Metro order:
+ // 'init BarV3',
+ // 'init FooV1',
+ 'init FooV1',
+ 'init BarV3',
+ // Webpack runs in this order because it evaluates modules parent down, not
+ // child up. Parents will re-run child modules in the order that they're
+ // imported from the parent.
+ ])
+
+ // We edited Bar so that it accepts itself.
+ // We still re-run Foo because the exports of Bar changed.
+ 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() {};
+ `
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ // // FIXME: Metro order:
+ // 'init BarV3',
+ // 'init FooV1',
+ 'init FooV1',
+ 'init BarV3',
+ // Webpack runs in this order because it evaluates modules parent down, not
+ // child up. Parents will re-run child modules in the order that they're
+ // imported from the parent.
+ ])
+
+ // Further edits to Bar don't re-run Foo.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ './bar.js',
+ `
+ window.log.push('init BarV4');
+ export default function Bar() {};
+ `
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV4',
+ ])
+
+ // TODO:
+ // expect(Refresh.performReactRefresh).toHaveBeenCalled();
+ // expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
+
+ await cleanup()
+ })
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1139-L1307
+ test('propagates hot update to all inverse dependencies', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ await session.patch(
+ 'index.js',
+ `export default function Noop() { return null; };`
+ )
+
+ // This is the module graph:
+ // MiddleA*
+ // / \
+ // Root* - MiddleB* - Leaf
+ // \
+ // MiddleC
+ //
+ // * - accepts update
+ //
+ // We expect that editing Leaf will propagate to
+ // MiddleA and MiddleB both of which can handle updates.
+
+ await session.write(
+ 'root.js',
+ `
+ window.log.push('init RootV1');
+
+ import './middleA';
+ import './middleB';
+ import './middleC';
+
+ export default function Root() {};
+ `
+ )
+ await session.write(
+ 'middleA.js',
+ `
+ log.push('init MiddleAV1');
+
+ import './leaf';
+
+ export default function MiddleA() {};
+ `
+ )
+ await session.write(
+ 'middleB.js',
+ `
+ log.push('init MiddleBV1');
+
+ import './leaf';
+
+ export default function MiddleB() {};
+ `
+ )
+ // This one doesn't import leaf and also doesn't export a component (so it
+ // doesn't accept updates).
+ await session.write(
+ 'middleC.js',
+ `log.push('init MiddleCV1'); export default {};`
+ )
+
+ // Doesn't accept its own updates; they will propagate.
+ await session.write(
+ 'leaf.js',
+ `log.push('init LeafV1'); export default {};`
+ )
+
+ // Bootstrap:
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ 'index.js',
+ `require('./root'); export default function Noop() { return null; };`
+ )
+
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init LeafV1',
+ 'init MiddleAV1',
+ 'init MiddleBV1',
+ 'init MiddleCV1',
+ 'init RootV1',
+ ])
+
+ // We edited Leaf, but it doesn't accept.
+ // So we expect it to re-run together with MiddleA and MiddleB which do.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ 'leaf.js',
+ `log.push('init LeafV2'); export default {};`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init LeafV2',
+ 'init MiddleAV1',
+ 'init MiddleBV1',
+ ])
+
+ // Let's try the same one more time.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ 'leaf.js',
+ `log.push('init LeafV3'); export default {};`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init LeafV3',
+ 'init MiddleAV1',
+ 'init MiddleBV1',
+ ])
+
+ // Now edit MiddleB. It should accept and re-run alone.
+ await session.evaluate(() => ((window as any).log = []))
+ await session.patch(
+ 'middleB.js',
+ `
+ log.push('init MiddleBV2');
+
+ import './leaf';
+
+ export default function MiddleB() {};
+ `
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init MiddleBV2',
+ ])
+
+ // Finally, edit MiddleC. It didn't accept so it should bubble to Root.
+ await session.evaluate(() => ((window as any).log = []))
+
+ await session.patch(
+ 'middleC.js',
+ `log.push('init MiddleCV2'); export default {};`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init MiddleCV2',
+ 'init RootV1',
+ ])
+
+ // TODO:
+ // expect(Refresh.performReactRefresh).toHaveBeenCalled()
+ // expect(Refresh.performFullRefresh).not.toHaveBeenCalled()
+
+ await cleanup()
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1309-L1406
+ test('runs dependencies before dependents', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1408-L1498
+ test('provides fresh value for module.exports in parents', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1500-L1590
+ test('provides fresh value for exports.* in parents', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1592-L1688
+ test('provides fresh value for ES6 named import in parents', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1690-L1786
+ test('provides fresh value for ES6 default import in parents', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1788-L1899
+ test('stops update propagation after module-level errors', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L1901-L2010
+ test('can continue hot updates after module-level errors with module.exports', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2012-L2123
+ test('can continue hot updates after module-level errors with ES6 exports', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2125-L2233
+ test('does not accumulate stale exports over time', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2235-L2279
+ test('bails out if update bubbles to the root via the only path', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2281-L2371
+ test('bails out if the update bubbles to the root via one of the paths', async () => {
+ // TODO:
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2373-L2472
+ // TODO-APP: investigate why it fails in app
+ test.skip('propagates a module that stops accepting in next version', async () => {
+ const { session, cleanup } = await sandbox(next)
+
+ // Accept in parent
+ await session.write(
+ './foo.js',
+ `;(typeof global !== 'undefined' ? global : window).log.push('init FooV1'); import './bar'; export default function Foo() {};`
+ )
+ // Accept in child
+ await session.write(
+ './bar.js',
+ `;(typeof global !== 'undefined' ? global : window).log.push('init BarV1'); export default function Bar() {};`
+ )
+
+ // Bootstrap:
+ await session.patch(
+ 'index.js',
+ `;(typeof global !== 'undefined' ? global : window).log = []; require('./foo'); export default () => null;`
+ )
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV1',
+ 'init FooV1',
+ ])
+
+ let didFullRefresh = false
+ // Verify the child can accept itself:
+ await session.evaluate(() => ((window as any).log = []))
+ didFullRefresh =
+ didFullRefresh ||
+ !(await session.patch(
+ './bar.js',
+ `window.log.push('init BarV1.1'); export default function Bar() {};`
+ ))
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV1.1',
+ ])
+
+ // Now let's change the child to *not* accept itself.
+ // We'll expect that now the parent will handle the evaluation.
+ await session.evaluate(() => ((window as any).log = []))
+ didFullRefresh =
+ didFullRefresh ||
+ !(await session.patch(
+ './bar.js',
+ // It's important we still export _something_, otherwise webpack will
+ // also emit an extra update to the parent module. This happens because
+ // webpack converts the module from ESM to CJS, which means the parent
+ // module must update how it "imports" the module (drops interop code).
+ // TODO: propose that webpack interrupts the current update phase when
+ // `module.hot.invalidate()` is called.
+ `window.log.push('init BarV2'); export {};`
+ ))
+ // We re-run Bar and expect to stop there. However,
+ // it didn't export a component, so we go higher.
+ // We stop at Foo which currently _does_ export a component.
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ // Bar evaluates twice:
+ // 1. To invalidate itself once it realizes it's no longer acceptable.
+ // 2. As a child of Foo re-evaluating.
+ 'init BarV2',
+ 'init BarV2',
+ 'init FooV1',
+ ])
+
+ // Change it back so that the child accepts itself.
+ await session.evaluate(() => ((window as any).log = []))
+ didFullRefresh =
+ didFullRefresh ||
+ !(await session.patch(
+ './bar.js',
+ `window.log.push('init BarV2'); export default function Bar() {};`
+ ))
+ // Since the export list changed, we have to re-run both the parent
+ // and the child.
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV2',
+ 'init FooV1',
+ ])
+
+ // TODO:
+ // expect(Refresh.performReactRefresh).toHaveBeenCalled();
+
+ // expect(Refresh.performFullRefresh).not.toHaveBeenCalled();
+ expect(didFullRefresh).toBe(false)
+
+ // But editing the child alone now doesn't reevaluate the parent.
+ await session.evaluate(() => ((window as any).log = []))
+ didFullRefresh =
+ didFullRefresh ||
+ !(await session.patch(
+ './bar.js',
+ `window.log.push('init BarV3'); export default function Bar() {};`
+ ))
+ expect(await session.evaluate(() => (window as any).log)).toEqual([
+ 'init BarV3',
+ ])
+
+ // Finally, edit the parent in a way that changes the export.
+ // It would still be accepted on its own -- but it's incompatible
+ // with the past version which didn't have two exports.
+ await session.evaluate(() => window.localStorage.setItem('init', ''))
+ didFullRefresh =
+ didFullRefresh ||
+ !(await session.patch(
+ './foo.js',
+ `
+ if (typeof window !== 'undefined' && window.localStorage) {
+ window.localStorage.setItem('init', 'init FooV2')
+ }
+ export function Foo() {};
+ export function FooFoo() {};`
+ ))
+
+ // Check that we attempted to evaluate, but had to fall back to full refresh.
+ expect(
+ await session.evaluate(() => window.localStorage.getItem('init'))
+ ).toEqual('init FooV2')
+
+ // expect(Refresh.performFullRefresh).toHaveBeenCalled();
+ expect(didFullRefresh).toBe(true)
+
+ await cleanup()
+ })
+
+ // https://github.com/facebook/metro/blob/b651e535cd0fc5df6c0803b9aa647d664cb9a6c3/packages/metro/src/lib/polyfills/__tests__/require-test.js#L2474-L2521
+ test('can replace a module before it is loaded', async () => {
+ // TODO:
+ })
+})
diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap
new file mode 100644
index 0000000000000..488a0c9376540
--- /dev/null
+++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap
@@ -0,0 +1,177 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ReactRefreshLogBox app boundaries 1`] = `
+"FunctionDefault.js (1:50) @ FunctionDefault
+
+> 1 | export default function FunctionDefault() { throw new Error('no'); }
+ | ^"
+`;
+
+exports[`ReactRefreshLogBox app 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 | "
+`;
+
+exports[`ReactRefreshLogBox app css syntax errors 1`] = `
+"./index.module.css:1:1
+Syntax error: Selector \\"button\\" is not pure (pure selectors must contain at least one local class or id)
+
+> 1 | button {}
+ | ^"
+`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 1`] = `"Error: end http://nextjs.org"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 2`] = `"http://nextjs.org/"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 3`] = `"Error: http://nextjs.org start"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 4`] = `"http://nextjs.org/"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 5`] = `"Error: middle http://nextjs.org end"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 6`] = `"http://nextjs.org/"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 8`] = `"http://nextjs.org/"`;
+
+exports[`ReactRefreshLogBox app logbox: anchors links in error messages 9`] = `"http://example.com/"`;
+
+exports[`ReactRefreshLogBox app logbox: can recover from a component error 1`] = `
+"child.js (4:16) @ Child
+
+ 2 | // hello
+ 3 | export default function Child() {
+> 4 | throw new Error('oops')
+ | ^
+ 5 | }
+ 6 | "
+`;
+
+exports[`ReactRefreshLogBox app logbox: can recover from a syntax error without losing state 1`] = `
+"./index.js
+Error:
+ x Unexpected eof
+ ,----
+ 1 | export default () =>
4 | throw new Error('no')
+ | ^
+ 5 | class ClassDefault extends React.Component {
+ 6 | render() {
+ 7 | return
Default Export
;"
+`;
+
+exports[`ReactRefreshLogBox app render error not shown right after syntax error 1`] = `
+"index.js (6:18) @ ClassDefault.render
+
+ 4 | class ClassDefault extends React.Component {
+ 5 | render() {
+> 6 | throw new Error('nooo');
+ | ^
+ 7 | return
Default Export
;
+ 8 | }
+ 9 | }"
+`;
+
+exports[`ReactRefreshLogBox app should strip whitespace correctly with newline 1`] = `
+"index.js (9:34) @ onClick
+
+ 7 |
+ 8 |
{
+> 9 | throw new Error('idk')
+ | ^
+ 10 | }}>
+ 11 | click me
+ 12 | "
+`;
+
+exports[`ReactRefreshLogBox app stuck error 1`] = `
+"Foo.js (4:10) @ Foo
+
+ 2 | // intentionally skips export
+ 3 | export default function Foo() {
+> 4 | return React.createElement('h1', null, 'Foo');
+ | ^
+ 5 | }
+ 6 | "
+`;
+
+exports[`ReactRefreshLogBox app syntax > runtime error 1`] = `
+"index.js (6:16) @ eval
+
+ 4 | setInterval(() => {
+ 5 | i++
+> 6 | throw Error('no ' + i)
+ | ^
+ 7 | }, 1000)
+ 8 | export default function FunctionNamed() {
+ 9 | return
"
+`;
+
+exports[`ReactRefreshLogBox app syntax > runtime error 2`] = `
+"./index.js
+Error:
+ x Expected '}', got '
'
+ ,----
+ 8 | export default function FunctionNamed() {
+ : ^
+ \`----
+
+Caused by:
+ 0: failed to process input file
+ 1: error was recoverable, but proceeding would result in wrong codegen
+ 2: Syntax Error"
+`;
+
+exports[`ReactRefreshLogBox app syntax > runtime error 3`] = `
+"./index.js
+Error:
+ x Expected '}', got ''
+ ,----
+ 8 | export default function FunctionNamed() {
+ : ^
+ \`----
+
+Caused by:
+ 0: failed to process input file
+ 1: error was recoverable, but proceeding would result in wrong codegen
+ 2: Syntax Error"
+`;
+
+exports[`ReactRefreshLogBox app unterminated JSX 1`] = `
+"./index.js
+Error:
+ x Unexpected token. Did you mean \`{'}'}\` or \`}\`?
+ ,----
+ 8 | }
+ : ^
+ \`----
+
+ x Unexpected eof
+ ,----
+ 9 |
+ : ^
+ \`----
+
+Caused by:
+ 0: failed to process input file
+ 1: Syntax Error"
+`;
diff --git a/test/development/acceptance-app/fixtures/default-template/app/layout.js b/test/development/acceptance-app/fixtures/default-template/app/layout.js
new file mode 100644
index 0000000000000..747270b45987a
--- /dev/null
+++ b/test/development/acceptance-app/fixtures/default-template/app/layout.js
@@ -0,0 +1,8 @@
+export default function RootLayout({ children }) {
+ return (
+
+
+ {children}
+
+ )
+}
diff --git a/test/development/acceptance-app/fixtures/default-template/app/page.js b/test/development/acceptance-app/fixtures/default-template/app/page.js
new file mode 100644
index 0000000000000..571a67124b063
--- /dev/null
+++ b/test/development/acceptance-app/fixtures/default-template/app/page.js
@@ -0,0 +1,5 @@
+'use client'
+import Component from '../index'
+export default function Page() {
+ return
+}
diff --git a/test/development/acceptance-app/fixtures/default-template/index.js b/test/development/acceptance-app/fixtures/default-template/index.js
new file mode 100644
index 0000000000000..31fd86d55937d
--- /dev/null
+++ b/test/development/acceptance-app/fixtures/default-template/index.js
@@ -0,0 +1 @@
+export default () => 'new sandbox'
diff --git a/test/development/acceptance-app/fixtures/default-template/next.config.js b/test/development/acceptance-app/fixtures/default-template/next.config.js
new file mode 100644
index 0000000000000..cfa3ac3d7aa94
--- /dev/null
+++ b/test/development/acceptance-app/fixtures/default-template/next.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ experimental: {
+ appDir: true,
+ },
+}
diff --git a/test/development/acceptance-app/helpers.ts b/test/development/acceptance-app/helpers.ts
new file mode 100644
index 0000000000000..0802671de7ead
--- /dev/null
+++ b/test/development/acceptance-app/helpers.ts
@@ -0,0 +1,121 @@
+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
+) {
+ await next.stop()
+ await next.clean()
+
+ if (initialFiles) {
+ for (const [k, v] of initialFiles.entries()) {
+ await next.patchFile(k, v)
+ }
+ }
+ await next.start()
+ const browser = await webdriver(next.appPort, '/')
+ 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 cleanup() {
+ await browser.close()
+ await next.stop()
+ await next.clean()
+ },
+ }
+}
diff --git a/test/e2e/app-dir/root-layout.test.ts b/test/e2e/app-dir/root-layout.test.ts
index 8acc240af87f9..976ab8318c414 100644
--- a/test/e2e/app-dir/root-layout.test.ts
+++ b/test/e2e/app-dir/root-layout.test.ts
@@ -31,7 +31,8 @@ describe('app-dir root layout', () => {
afterAll(() => next.destroy())
if (isDev) {
- describe('Missing required tags', () => {
+ // TODO-APP: re-enable after reworking the error overlay.
+ describe.skip('Missing required tags', () => {
it('should error on page load', async () => {
const browser = await webdriver(next.url, '/missing-tags', {
waitHydration: false,
diff --git a/test/lib/next-modes/base.ts b/test/lib/next-modes/base.ts
index d703f9e69e581..964fc9b1021aa 100644
--- a/test/lib/next-modes/base.ts
+++ b/test/lib/next-modes/base.ts
@@ -54,6 +54,33 @@ export class NextInstance {
Object.assign(this, opts)
}
+ protected async writeInitialFiles() {
+ if (this.files instanceof FileRef) {
+ // if a FileRef is passed directly to `files` we copy the
+ // entire folder to the test directory
+ const stats = await fs.stat(this.files.fsPath)
+
+ if (!stats.isDirectory()) {
+ throw new Error(
+ `FileRef passed to "files" in "createNext" is not a directory ${this.files.fsPath}`
+ )
+ }
+ await fs.copy(this.files.fsPath, this.testDir)
+ } else {
+ for (const filename of Object.keys(this.files)) {
+ const item = this.files[filename]
+ const outputFilename = path.join(this.testDir, filename)
+
+ if (typeof item === 'string') {
+ await fs.ensureDir(path.dirname(outputFilename))
+ await fs.writeFile(outputFilename, item)
+ } else {
+ await fs.copy(item.fsPath, outputFilename)
+ }
+ }
+ }
+ }
+
protected async createTestDir({
skipInstall = false,
}: { skipInstall?: boolean } = {}) {
@@ -128,30 +155,7 @@ export class NextInstance {
require('console').log('created next.js install, writing test files')
}
- if (this.files instanceof FileRef) {
- // if a FileRef is passed directly to `files` we copy the
- // entire folder to the test directory
- const stats = await fs.stat(this.files.fsPath)
-
- if (!stats.isDirectory()) {
- throw new Error(
- `FileRef passed to "files" in "createNext" is not a directory ${this.files.fsPath}`
- )
- }
- await fs.copy(this.files.fsPath, this.testDir)
- } else {
- for (const filename of Object.keys(this.files)) {
- const item = this.files[filename]
- const outputFilename = path.join(this.testDir, filename)
-
- if (typeof item === 'string') {
- await fs.ensureDir(path.dirname(outputFilename))
- await fs.writeFile(outputFilename, item)
- } else {
- await fs.copy(item.fsPath, outputFilename)
- }
- }
- }
+ await this.writeInitialFiles()
let nextConfigFile = Object.keys(this.files).find((file) =>
file.startsWith('next.config.')
@@ -234,6 +238,7 @@ export class NextInstance {
await fs.remove(path.join(this.testDir, file))
}
}
+ await this.writeInitialFiles()
}
public async export(): Promise<{ exitCode?: number; cliOutput?: string }> {