diff --git a/.eslintignore b/.eslintignore index 98984d851d1f7..e6acc5e8811e3 100644 --- a/.eslintignore +++ b/.eslintignore @@ -41,7 +41,7 @@ bench/nested-deps-app-router/** bench/heavy-npm-deps/** packages/next-bundle-analyzer/index.d.ts examples/with-typescript-graphql/lib/gql/ -test/development/basic/hmr/components/parse-error.js +test/development/basic/hmr/fixtures/components/parse-error.js packages/next-swc/docs/assets/**/* test/lib/amp-validator-wasm.js test/production/pages-dir/production/fixture/amp-validator-wasm.js diff --git a/.prettierignore b/.prettierignore index 456a590ab8d5c..e0e9122d63301 100644 --- a/.prettierignore +++ b/.prettierignore @@ -38,7 +38,7 @@ packages/next-codemod/**/*.d.ts packages/next-env/**/*.d.ts test/**/out/** -test/development/basic/hmr/components/parse-error.js +test/development/basic/hmr/fixtures/components/parse-error.js bench/nested-deps/pages/**/* bench/nested-deps/components/**/* diff --git a/test/development/basic/hmr/basic.test.ts b/test/development/basic/hmr/basic-no-base-path-no-asset-prefix.test.ts similarity index 90% rename from test/development/basic/hmr/basic.test.ts rename to test/development/basic/hmr/basic-no-base-path-no-asset-prefix.test.ts index acdb082cf9853..6cef73f26924c 100644 --- a/test/development/basic/hmr/basic.test.ts +++ b/test/development/basic/hmr/basic-no-base-path-no-asset-prefix.test.ts @@ -1,3 +1,4 @@ +import { join } from 'path' import { assertHasRedbox, getBrowserBodyText, @@ -5,16 +6,12 @@ import { waitFor, } from 'next-test-utils' import { createNext, nextTestSetup } from 'e2e-utils' -import type { NextConfig } from 'next' - -describe.each([ - { basePath: '', assetPrefix: '' }, - { basePath: '', assetPrefix: '/asset-prefix' }, - { basePath: '/docs', assetPrefix: '' }, - { basePath: '/docs', assetPrefix: '/asset-prefix' }, -])('basic HMR, nextConfig: %o', (nextConfig: Partial) => { + +const nextConfig = { basePath: '', assetPrefix: '' } + +describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => { const { next } = nextTestSetup({ - files: __dirname, + files: join(__dirname, '../../fixtures'), nextConfig, patchFileDelay: 500, }) @@ -105,7 +102,7 @@ describe.each([ }) const secondNext = await createNext({ - files: __dirname, + files: join(__dirname, '../../fixtures'), nextConfig, forcedPort: next.appPort, }) diff --git a/test/development/basic/hmr/basic-no-base-path-yes-asset-prefix.test.ts b/test/development/basic/hmr/basic-no-base-path-yes-asset-prefix.test.ts new file mode 100644 index 0000000000000..7187168823eba --- /dev/null +++ b/test/development/basic/hmr/basic-no-base-path-yes-asset-prefix.test.ts @@ -0,0 +1,113 @@ +import { join } from 'path' +import { + assertHasRedbox, + getBrowserBodyText, + retry, + waitFor, +} from 'next-test-utils' +import { createNext, nextTestSetup } from 'e2e-utils' + +const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' } + +describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should have correct router.isReady for auto-export page', async () => { + let browser = await next.browser(basePath + '/auto-export-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await next.browser(basePath + '/auto-export-is-ready?hello=world') + + await retry(async () => { + expect(await browser.elementByCss('#ready').text()).toBe('yes') + }) + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', + }) + }) + + it('should have correct router.isReady for getStaticProps page', async () => { + let browser = await next.browser(basePath + '/gsp-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await next.browser(basePath + '/gsp-is-ready?hello=world') + + await retry(async () => { + expect(await browser.elementByCss('#ready').text()).toBe('yes') + }) + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', + }) + }) + + it('should have correct compile timing after fixing error', async () => { + const pageName = 'pages/auto-export-is-ready.js' + const originalContent = await next.readFile(pageName) + + try { + const browser = await next.browser(basePath + '/auto-export-is-ready') + const outputLength = next.cliOutput.length + await next.patchFile( + pageName, + `import hello from 'non-existent'\n` + originalContent + ) + await assertHasRedbox(browser) + await waitFor(3000) + await next.patchFile(pageName, originalContent) + await retry(async () => { + expect(next.cliOutput.substring(outputLength)).toMatch(/Compiled.*?/i) + }) + const compileTimeStr = next.cliOutput.substring(outputLength) + + const matches = [ + ...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i), + ] + const [, compileTime, timeUnit] = matches + + let compileTimeMs = parseFloat(compileTime) + if (timeUnit === 's') { + compileTimeMs = compileTimeMs * 1000 + } + expect(compileTimeMs).toBeLessThan(3000) + } finally { + await next.patchFile(pageName, originalContent) + } + }) + + it('should reload the page when the server restarts', async () => { + const browser = await next.browser(basePath + '/hmr/about') + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.destroy() + + let reloadPromise = new Promise((resolve) => { + browser.on('request', (req) => { + if (req.url().endsWith('/hmr/about')) { + resolve(req.url()) + } + }) + }) + + const secondNext = await createNext({ + files: join(__dirname, '../../fixtures'), + nextConfig, + forcedPort: next.appPort, + }) + + await reloadPromise + await secondNext.destroy() + }) +}) diff --git a/test/development/basic/hmr/basic-yes-base-path-no-asset-prefix.test.ts b/test/development/basic/hmr/basic-yes-base-path-no-asset-prefix.test.ts new file mode 100644 index 0000000000000..233ad493f6fbf --- /dev/null +++ b/test/development/basic/hmr/basic-yes-base-path-no-asset-prefix.test.ts @@ -0,0 +1,113 @@ +import { join } from 'path' +import { + assertHasRedbox, + getBrowserBodyText, + retry, + waitFor, +} from 'next-test-utils' +import { createNext, nextTestSetup } from 'e2e-utils' + +const nextConfig = { basePath: '/docs', assetPrefix: '' } + +describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should have correct router.isReady for auto-export page', async () => { + let browser = await next.browser(basePath + '/auto-export-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await next.browser(basePath + '/auto-export-is-ready?hello=world') + + await retry(async () => { + expect(await browser.elementByCss('#ready').text()).toBe('yes') + }) + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', + }) + }) + + it('should have correct router.isReady for getStaticProps page', async () => { + let browser = await next.browser(basePath + '/gsp-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await next.browser(basePath + '/gsp-is-ready?hello=world') + + await retry(async () => { + expect(await browser.elementByCss('#ready').text()).toBe('yes') + }) + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', + }) + }) + + it('should have correct compile timing after fixing error', async () => { + const pageName = 'pages/auto-export-is-ready.js' + const originalContent = await next.readFile(pageName) + + try { + const browser = await next.browser(basePath + '/auto-export-is-ready') + const outputLength = next.cliOutput.length + await next.patchFile( + pageName, + `import hello from 'non-existent'\n` + originalContent + ) + await assertHasRedbox(browser) + await waitFor(3000) + await next.patchFile(pageName, originalContent) + await retry(async () => { + expect(next.cliOutput.substring(outputLength)).toMatch(/Compiled.*?/i) + }) + const compileTimeStr = next.cliOutput.substring(outputLength) + + const matches = [ + ...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i), + ] + const [, compileTime, timeUnit] = matches + + let compileTimeMs = parseFloat(compileTime) + if (timeUnit === 's') { + compileTimeMs = compileTimeMs * 1000 + } + expect(compileTimeMs).toBeLessThan(3000) + } finally { + await next.patchFile(pageName, originalContent) + } + }) + + it('should reload the page when the server restarts', async () => { + const browser = await next.browser(basePath + '/hmr/about') + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.destroy() + + let reloadPromise = new Promise((resolve) => { + browser.on('request', (req) => { + if (req.url().endsWith('/hmr/about')) { + resolve(req.url()) + } + }) + }) + + const secondNext = await createNext({ + files: join(__dirname, '../../fixtures'), + nextConfig, + forcedPort: next.appPort, + }) + + await reloadPromise + await secondNext.destroy() + }) +}) diff --git a/test/development/basic/hmr/basic-yes-base-path-yes-asset-prefix.test.ts b/test/development/basic/hmr/basic-yes-base-path-yes-asset-prefix.test.ts new file mode 100644 index 0000000000000..025a10bcd3163 --- /dev/null +++ b/test/development/basic/hmr/basic-yes-base-path-yes-asset-prefix.test.ts @@ -0,0 +1,113 @@ +import { join } from 'path' +import { + assertHasRedbox, + getBrowserBodyText, + retry, + waitFor, +} from 'next-test-utils' +import { createNext, nextTestSetup } from 'e2e-utils' + +const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' } + +describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should have correct router.isReady for auto-export page', async () => { + let browser = await next.browser(basePath + '/auto-export-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await next.browser(basePath + '/auto-export-is-ready?hello=world') + + await retry(async () => { + expect(await browser.elementByCss('#ready').text()).toBe('yes') + }) + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', + }) + }) + + it('should have correct router.isReady for getStaticProps page', async () => { + let browser = await next.browser(basePath + '/gsp-is-ready') + + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) + + browser = await next.browser(basePath + '/gsp-is-ready?hello=world') + + await retry(async () => { + expect(await browser.elementByCss('#ready').text()).toBe('yes') + }) + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', + }) + }) + + it('should have correct compile timing after fixing error', async () => { + const pageName = 'pages/auto-export-is-ready.js' + const originalContent = await next.readFile(pageName) + + try { + const browser = await next.browser(basePath + '/auto-export-is-ready') + const outputLength = next.cliOutput.length + await next.patchFile( + pageName, + `import hello from 'non-existent'\n` + originalContent + ) + await assertHasRedbox(browser) + await waitFor(3000) + await next.patchFile(pageName, originalContent) + await retry(async () => { + expect(next.cliOutput.substring(outputLength)).toMatch(/Compiled.*?/i) + }) + const compileTimeStr = next.cliOutput.substring(outputLength) + + const matches = [ + ...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i), + ] + const [, compileTime, timeUnit] = matches + + let compileTimeMs = parseFloat(compileTime) + if (timeUnit === 's') { + compileTimeMs = compileTimeMs * 1000 + } + expect(compileTimeMs).toBeLessThan(3000) + } finally { + await next.patchFile(pageName, originalContent) + } + }) + + it('should reload the page when the server restarts', async () => { + const browser = await next.browser(basePath + '/hmr/about') + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.destroy() + + let reloadPromise = new Promise((resolve) => { + browser.on('request', (req) => { + if (req.url().endsWith('/hmr/about')) { + resolve(req.url()) + } + }) + }) + + const secondNext = await createNext({ + files: join(__dirname, '../../fixtures'), + nextConfig, + forcedPort: next.appPort, + }) + + await reloadPromise + await secondNext.destroy() + }) +}) diff --git a/test/development/basic/hmr/components/hmr/dynamic.js b/test/development/basic/hmr/components/hmr/dynamic.js deleted file mode 100644 index 1e383981bbcb1..0000000000000 --- a/test/development/basic/hmr/components/hmr/dynamic.js +++ /dev/null @@ -1,12 +0,0 @@ -export default () => { - return ( -
- Dynamic Component - -
- ) -} diff --git a/test/development/basic/hmr/components/parse-error.js b/test/development/basic/hmr/components/parse-error.js deleted file mode 100644 index 02bc2a71c8b57..0000000000000 --- a/test/development/basic/hmr/components/parse-error.js +++ /dev/null @@ -1,5 +0,0 @@ -This -is -}}} -invalid -js \ No newline at end of file diff --git a/test/development/basic/hmr/components/parse-error.xyz b/test/development/basic/hmr/components/parse-error.xyz deleted file mode 100644 index 02bc2a71c8b57..0000000000000 --- a/test/development/basic/hmr/components/parse-error.xyz +++ /dev/null @@ -1,5 +0,0 @@ -This -is -}}} -invalid -js \ No newline at end of file diff --git a/test/development/basic/hmr/error-recovery-no-base-path-no-asset-prefix.test.ts b/test/development/basic/hmr/error-recovery-no-base-path-no-asset-prefix.test.ts new file mode 100644 index 0000000000000..bfd16c18f78cf --- /dev/null +++ b/test/development/basic/hmr/error-recovery-no-base-path-no-asset-prefix.test.ts @@ -0,0 +1,728 @@ +import { join } from 'path' +import { + assertHasRedbox, + assertNoRedbox, + getBrowserBodyText, + getRedboxHeader, + getRedboxDescription, + getRedboxSource, + renderViaHTTP, + retry, + waitFor, +} from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' +import { outdent } from 'outdent' + +const nextConfig = { basePath: '', assetPrefix: '' } + +describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should recover from 404 after a page has been added', async () => { + const newPage = join('pages', 'hmr', 'new-page.js') + + try { + const browser = await next.browser(basePath + '/hmr/new-page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + + it('should recover from 404 after a page has been added with dynamic segments', async () => { + const newPage = join('pages', 'hmr', '[foo]', 'page.js') + + try { + const browser = await next.browser(basePath + '/hmr/foo/page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + ;(process.env.TURBOPACK ? it.skip : it)( + // this test fails frequently with turbopack + 'should not continously poll a custom error page', + async () => { + const errorPage = join('pages', '_error.js') + + await next.patchFile( + errorPage, + outdent` + function Error({ statusCode, message, count }) { + return ( +
+ Error Message: {message} +
+ ) + } + + Error.getInitialProps = async ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404 + console.log('getInitialProps called'); + return { + statusCode, + message: err ? err.message : 'Oops...', + } + } + + export default Error + ` + ) + + try { + // navigate to a 404 page + await next.browser(basePath + '/does-not-exist') + + await retry(() => { + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput).toMatch(/getInitialProps called/) + }) + + const outputIndex = next.cliOutput.length + + // wait a few seconds to ensure polling didn't happen + await waitFor(3000) + + const logOccurrences = + next.cliOutput.slice(outputIndex).split('getInitialProps called') + .length - 1 + // eslint-disable-next-line jest/no-standalone-expect + expect(logOccurrences).toBe(0) + } finally { + await next.deleteFile(errorPage) + } + } + ) + + it('should detect syntax errors and recover', async () => { + const browser = await next.browser(basePath + '/hmr/about2') + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + await assertHasRedbox(browser) + const source = next.normalizeTestDirContent(await getRedboxSource(browser)) + if (basePath === '' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } else if (basePath === '/docs' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '/docs' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + }) + + if (!process.env.TURBOPACK) { + // Turbopack doesn't have this restriction + it('should show the error on all pages', async () => { + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + const browser = await next.browser(basePath + '/hmr/contact') + try { + await renderViaHTTP(next.url, basePath + '/hmr/about2') + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + // Ensure dev server has time to break: + await new Promise((resolve) => setTimeout(resolve, 2000)) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } + + throw err + } + }) + } + + it('should detect runtime errors on the module scope', async () => { + const browser = await next.browser(basePath + '/hmr/about3') + const aboutPage = join('pages', 'hmr', 'about3.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace('export', 'aa=20;\nexport') + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } finally { + await next.patchFile(aboutPage, aboutContent) + } + }) + + it('should recover from errors in the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about4') + const aboutPage = join('pages', 'hmr', 'about4.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'return', + 'throw new Error("an-expected-error");\nreturn' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after exporting an invalid page', async () => { + const browser = await next.browser(basePath + '/hmr/about5') + const aboutPage = join('pages', 'hmr', 'about5.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default {};\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about5""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after a bad return from the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about6') + const aboutPage = join('pages', 'hmr', 'about6.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default () => /search/;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + // TODO: Replace this when webpack 5 is the default + expect(await getRedboxHeader(browser)).toMatch( + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after undefined exported as default', async () => { + const browser = await next.browser(basePath + '/hmr/about7') + const aboutPage = join('pages', 'hmr', 'about7.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default undefined;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about7""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after webpack parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about8') + const aboutPage = join('pages', 'hmr', 'about8.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.xyz"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + + if (process.env.TURBOPACK) { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Unknown module type + This module doesn't have an associated type. Use a known file extension, or register a loader for it. + + Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders" + `) + } else { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Module parse failed: Unexpected token (3:0) + You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders + | This + | is + > }}} + | invalid + | js + + Import trace for requested module: + ./components/parse-error.xyz + ./pages/hmr/about8.js" + `) + } + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after loader parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about9') + const aboutPage = join('pages', 'hmr', 'about9.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.js"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + let redboxSource = await getRedboxSource(browser) + + redboxSource = redboxSource.replace(`${next.testDir}`, '.') + if (process.env.TURBOPACK) { + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js:3:1 + Parsing ecmascript source code failed + 1 | This + 2 | is + > 3 | }}} + | ^ + 4 | invalid + 5 | js + + Expression expected" + `) + } else { + redboxSource = redboxSource.substring(0, redboxSource.indexOf('`----')) + + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js + Error: x Expression expected + ,-[3:1] + 1 | This + 2 | is + 3 | }}} + : ^ + 4 | invalid + 5 | js + " + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } + }) + + it('should recover from errors in getInitialProps in client', async () => { + const browser = await next.browser(basePath + '/hmr') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await browser.elementByCss('#error-in-gip-link').click() + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + it('should recover after an error reported via SSR', async () => { + const browser = await next.browser(basePath + '/hmr/error-in-gip') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + if (!process.env.TURBOPACK) { + it('should have client HMR events in trace file', async () => { + const traceData = await next.readFile('.next/trace') + expect(traceData).toContain('client-hmr-latency') + expect(traceData).toContain('client-error') + expect(traceData).toContain('client-success') + expect(traceData).toContain('client-full-reload') + }) + } +}) diff --git a/test/development/basic/hmr/error-recovery-no-base-path-yes-asset-prefix.test.ts b/test/development/basic/hmr/error-recovery-no-base-path-yes-asset-prefix.test.ts new file mode 100644 index 0000000000000..254ead9d5809f --- /dev/null +++ b/test/development/basic/hmr/error-recovery-no-base-path-yes-asset-prefix.test.ts @@ -0,0 +1,728 @@ +import { join } from 'path' +import { + assertHasRedbox, + assertNoRedbox, + getBrowserBodyText, + getRedboxHeader, + getRedboxDescription, + getRedboxSource, + renderViaHTTP, + retry, + waitFor, +} from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' +import { outdent } from 'outdent' + +const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' } + +describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should recover from 404 after a page has been added', async () => { + const newPage = join('pages', 'hmr', 'new-page.js') + + try { + const browser = await next.browser(basePath + '/hmr/new-page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + + it('should recover from 404 after a page has been added with dynamic segments', async () => { + const newPage = join('pages', 'hmr', '[foo]', 'page.js') + + try { + const browser = await next.browser(basePath + '/hmr/foo/page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + ;(process.env.TURBOPACK ? it.skip : it)( + // this test fails frequently with turbopack + 'should not continously poll a custom error page', + async () => { + const errorPage = join('pages', '_error.js') + + await next.patchFile( + errorPage, + outdent` + function Error({ statusCode, message, count }) { + return ( +
+ Error Message: {message} +
+ ) + } + + Error.getInitialProps = async ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404 + console.log('getInitialProps called'); + return { + statusCode, + message: err ? err.message : 'Oops...', + } + } + + export default Error + ` + ) + + try { + // navigate to a 404 page + await next.browser(basePath + '/does-not-exist') + + await retry(() => { + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput).toMatch(/getInitialProps called/) + }) + + const outputIndex = next.cliOutput.length + + // wait a few seconds to ensure polling didn't happen + await waitFor(3000) + + const logOccurrences = + next.cliOutput.slice(outputIndex).split('getInitialProps called') + .length - 1 + // eslint-disable-next-line jest/no-standalone-expect + expect(logOccurrences).toBe(0) + } finally { + await next.deleteFile(errorPage) + } + } + ) + + it('should detect syntax errors and recover', async () => { + const browser = await next.browser(basePath + '/hmr/about2') + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + await assertHasRedbox(browser) + const source = next.normalizeTestDirContent(await getRedboxSource(browser)) + if (basePath === '' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } else if (basePath === '/docs' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '/docs' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + }) + + if (!process.env.TURBOPACK) { + // Turbopack doesn't have this restriction + it('should show the error on all pages', async () => { + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + const browser = await next.browser(basePath + '/hmr/contact') + try { + await renderViaHTTP(next.url, basePath + '/hmr/about2') + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + // Ensure dev server has time to break: + await new Promise((resolve) => setTimeout(resolve, 2000)) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } + + throw err + } + }) + } + + it('should detect runtime errors on the module scope', async () => { + const browser = await next.browser(basePath + '/hmr/about3') + const aboutPage = join('pages', 'hmr', 'about3.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace('export', 'aa=20;\nexport') + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } finally { + await next.patchFile(aboutPage, aboutContent) + } + }) + + it('should recover from errors in the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about4') + const aboutPage = join('pages', 'hmr', 'about4.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'return', + 'throw new Error("an-expected-error");\nreturn' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after exporting an invalid page', async () => { + const browser = await next.browser(basePath + '/hmr/about5') + const aboutPage = join('pages', 'hmr', 'about5.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default {};\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about5""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after a bad return from the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about6') + const aboutPage = join('pages', 'hmr', 'about6.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default () => /search/;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + // TODO: Replace this when webpack 5 is the default + expect(await getRedboxHeader(browser)).toMatch( + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after undefined exported as default', async () => { + const browser = await next.browser(basePath + '/hmr/about7') + const aboutPage = join('pages', 'hmr', 'about7.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default undefined;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about7""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after webpack parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about8') + const aboutPage = join('pages', 'hmr', 'about8.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.xyz"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + + if (process.env.TURBOPACK) { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Unknown module type + This module doesn't have an associated type. Use a known file extension, or register a loader for it. + + Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders" + `) + } else { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Module parse failed: Unexpected token (3:0) + You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders + | This + | is + > }}} + | invalid + | js + + Import trace for requested module: + ./components/parse-error.xyz + ./pages/hmr/about8.js" + `) + } + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after loader parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about9') + const aboutPage = join('pages', 'hmr', 'about9.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.js"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + let redboxSource = await getRedboxSource(browser) + + redboxSource = redboxSource.replace(`${next.testDir}`, '.') + if (process.env.TURBOPACK) { + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js:3:1 + Parsing ecmascript source code failed + 1 | This + 2 | is + > 3 | }}} + | ^ + 4 | invalid + 5 | js + + Expression expected" + `) + } else { + redboxSource = redboxSource.substring(0, redboxSource.indexOf('`----')) + + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js + Error: x Expression expected + ,-[3:1] + 1 | This + 2 | is + 3 | }}} + : ^ + 4 | invalid + 5 | js + " + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } + }) + + it('should recover from errors in getInitialProps in client', async () => { + const browser = await next.browser(basePath + '/hmr') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await browser.elementByCss('#error-in-gip-link').click() + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + it('should recover after an error reported via SSR', async () => { + const browser = await next.browser(basePath + '/hmr/error-in-gip') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + if (!process.env.TURBOPACK) { + it('should have client HMR events in trace file', async () => { + const traceData = await next.readFile('.next/trace') + expect(traceData).toContain('client-hmr-latency') + expect(traceData).toContain('client-error') + expect(traceData).toContain('client-success') + expect(traceData).toContain('client-full-reload') + }) + } +}) diff --git a/test/development/basic/hmr/error-recovery-yes-base-path-no-asset-prefix.test.ts b/test/development/basic/hmr/error-recovery-yes-base-path-no-asset-prefix.test.ts new file mode 100644 index 0000000000000..5ea3393f30516 --- /dev/null +++ b/test/development/basic/hmr/error-recovery-yes-base-path-no-asset-prefix.test.ts @@ -0,0 +1,728 @@ +import { join } from 'path' +import { + assertHasRedbox, + assertNoRedbox, + getBrowserBodyText, + getRedboxHeader, + getRedboxDescription, + getRedboxSource, + renderViaHTTP, + retry, + waitFor, +} from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' +import { outdent } from 'outdent' + +const nextConfig = { basePath: '/docs', assetPrefix: '' } + +describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should recover from 404 after a page has been added', async () => { + const newPage = join('pages', 'hmr', 'new-page.js') + + try { + const browser = await next.browser(basePath + '/hmr/new-page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + + it('should recover from 404 after a page has been added with dynamic segments', async () => { + const newPage = join('pages', 'hmr', '[foo]', 'page.js') + + try { + const browser = await next.browser(basePath + '/hmr/foo/page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + ;(process.env.TURBOPACK ? it.skip : it)( + // this test fails frequently with turbopack + 'should not continously poll a custom error page', + async () => { + const errorPage = join('pages', '_error.js') + + await next.patchFile( + errorPage, + outdent` + function Error({ statusCode, message, count }) { + return ( +
+ Error Message: {message} +
+ ) + } + + Error.getInitialProps = async ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404 + console.log('getInitialProps called'); + return { + statusCode, + message: err ? err.message : 'Oops...', + } + } + + export default Error + ` + ) + + try { + // navigate to a 404 page + await next.browser(basePath + '/does-not-exist') + + await retry(() => { + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput).toMatch(/getInitialProps called/) + }) + + const outputIndex = next.cliOutput.length + + // wait a few seconds to ensure polling didn't happen + await waitFor(3000) + + const logOccurrences = + next.cliOutput.slice(outputIndex).split('getInitialProps called') + .length - 1 + // eslint-disable-next-line jest/no-standalone-expect + expect(logOccurrences).toBe(0) + } finally { + await next.deleteFile(errorPage) + } + } + ) + + it('should detect syntax errors and recover', async () => { + const browser = await next.browser(basePath + '/hmr/about2') + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + await assertHasRedbox(browser) + const source = next.normalizeTestDirContent(await getRedboxSource(browser)) + if (basePath === '' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } else if (basePath === '/docs' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '/docs' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + }) + + if (!process.env.TURBOPACK) { + // Turbopack doesn't have this restriction + it('should show the error on all pages', async () => { + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + const browser = await next.browser(basePath + '/hmr/contact') + try { + await renderViaHTTP(next.url, basePath + '/hmr/about2') + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + // Ensure dev server has time to break: + await new Promise((resolve) => setTimeout(resolve, 2000)) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } + + throw err + } + }) + } + + it('should detect runtime errors on the module scope', async () => { + const browser = await next.browser(basePath + '/hmr/about3') + const aboutPage = join('pages', 'hmr', 'about3.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace('export', 'aa=20;\nexport') + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } finally { + await next.patchFile(aboutPage, aboutContent) + } + }) + + it('should recover from errors in the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about4') + const aboutPage = join('pages', 'hmr', 'about4.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'return', + 'throw new Error("an-expected-error");\nreturn' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after exporting an invalid page', async () => { + const browser = await next.browser(basePath + '/hmr/about5') + const aboutPage = join('pages', 'hmr', 'about5.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default {};\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about5""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after a bad return from the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about6') + const aboutPage = join('pages', 'hmr', 'about6.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default () => /search/;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + // TODO: Replace this when webpack 5 is the default + expect(await getRedboxHeader(browser)).toMatch( + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after undefined exported as default', async () => { + const browser = await next.browser(basePath + '/hmr/about7') + const aboutPage = join('pages', 'hmr', 'about7.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default undefined;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about7""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after webpack parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about8') + const aboutPage = join('pages', 'hmr', 'about8.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.xyz"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + + if (process.env.TURBOPACK) { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Unknown module type + This module doesn't have an associated type. Use a known file extension, or register a loader for it. + + Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders" + `) + } else { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Module parse failed: Unexpected token (3:0) + You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders + | This + | is + > }}} + | invalid + | js + + Import trace for requested module: + ./components/parse-error.xyz + ./pages/hmr/about8.js" + `) + } + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after loader parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about9') + const aboutPage = join('pages', 'hmr', 'about9.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.js"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + let redboxSource = await getRedboxSource(browser) + + redboxSource = redboxSource.replace(`${next.testDir}`, '.') + if (process.env.TURBOPACK) { + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js:3:1 + Parsing ecmascript source code failed + 1 | This + 2 | is + > 3 | }}} + | ^ + 4 | invalid + 5 | js + + Expression expected" + `) + } else { + redboxSource = redboxSource.substring(0, redboxSource.indexOf('`----')) + + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js + Error: x Expression expected + ,-[3:1] + 1 | This + 2 | is + 3 | }}} + : ^ + 4 | invalid + 5 | js + " + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } + }) + + it('should recover from errors in getInitialProps in client', async () => { + const browser = await next.browser(basePath + '/hmr') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await browser.elementByCss('#error-in-gip-link').click() + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + it('should recover after an error reported via SSR', async () => { + const browser = await next.browser(basePath + '/hmr/error-in-gip') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + if (!process.env.TURBOPACK) { + it('should have client HMR events in trace file', async () => { + const traceData = await next.readFile('.next/trace') + expect(traceData).toContain('client-hmr-latency') + expect(traceData).toContain('client-error') + expect(traceData).toContain('client-success') + expect(traceData).toContain('client-full-reload') + }) + } +}) diff --git a/test/development/basic/hmr/error-recovery-yes-base-path-yes-asset-prefix.test.ts b/test/development/basic/hmr/error-recovery-yes-base-path-yes-asset-prefix.test.ts new file mode 100644 index 0000000000000..3be32cc05c140 --- /dev/null +++ b/test/development/basic/hmr/error-recovery-yes-base-path-yes-asset-prefix.test.ts @@ -0,0 +1,728 @@ +import { join } from 'path' +import { + assertHasRedbox, + assertNoRedbox, + getBrowserBodyText, + getRedboxHeader, + getRedboxDescription, + getRedboxSource, + renderViaHTTP, + retry, + waitFor, +} from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' +import { outdent } from 'outdent' + +const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' } + +describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should recover from 404 after a page has been added', async () => { + const newPage = join('pages', 'hmr', 'new-page.js') + + try { + const browser = await next.browser(basePath + '/hmr/new-page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + + it('should recover from 404 after a page has been added with dynamic segments', async () => { + const newPage = join('pages', 'hmr', '[foo]', 'page.js') + + try { + const browser = await next.browser(basePath + '/hmr/foo/page') + + expect(await browser.elementByCss('body').text()).toMatch( + /This page could not be found/ + ) + + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) + }) + + await next.deleteFile(newPage) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } + }) + ;(process.env.TURBOPACK ? it.skip : it)( + // this test fails frequently with turbopack + 'should not continously poll a custom error page', + async () => { + const errorPage = join('pages', '_error.js') + + await next.patchFile( + errorPage, + outdent` + function Error({ statusCode, message, count }) { + return ( +
+ Error Message: {message} +
+ ) + } + + Error.getInitialProps = async ({ res, err }) => { + const statusCode = res ? res.statusCode : err ? err.statusCode : 404 + console.log('getInitialProps called'); + return { + statusCode, + message: err ? err.message : 'Oops...', + } + } + + export default Error + ` + ) + + try { + // navigate to a 404 page + await next.browser(basePath + '/does-not-exist') + + await retry(() => { + // eslint-disable-next-line jest/no-standalone-expect + expect(next.cliOutput).toMatch(/getInitialProps called/) + }) + + const outputIndex = next.cliOutput.length + + // wait a few seconds to ensure polling didn't happen + await waitFor(3000) + + const logOccurrences = + next.cliOutput.slice(outputIndex).split('getInitialProps called') + .length - 1 + // eslint-disable-next-line jest/no-standalone-expect + expect(logOccurrences).toBe(0) + } finally { + await next.deleteFile(errorPage) + } + } + ) + + it('should detect syntax errors and recover', async () => { + const browser = await next.browser(basePath + '/hmr/about2') + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + await assertHasRedbox(browser) + const source = next.normalizeTestDirContent(await getRedboxSource(browser)) + if (basePath === '' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } else if (basePath === '/docs' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js + Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? + ,-[7:1] + 4 |

This is the about page.

+ 5 | div + 6 | ) + 7 | } + : ^ + \`---- + x Unexpected eof + ,-[7:3] + 5 | div + 6 | ) + 7 | } + \`---- + + Caused by: + Syntax Error + + Import trace for requested module: + ./pages/hmr/about2.js" + `) + } else if (basePath === '/docs' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` + "./pages/hmr/about2.js:7:1 + Parsing ecmascript source code failed + 5 | div + 6 | ) + > 7 | } + | ^ + 8 | + + Unexpected token. Did you mean \`{'}'}\` or \`}\`?" + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + }) + + if (!process.env.TURBOPACK) { + // Turbopack doesn't have this restriction + it('should show the error on all pages', async () => { + const aboutPage = join('pages', 'hmr', 'about2.js') + const aboutContent = await next.readFile(aboutPage) + const browser = await next.browser(basePath + '/hmr/contact') + try { + await renderViaHTTP(next.url, basePath + '/hmr/about2') + + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) + + // Ensure dev server has time to break: + await new Promise((resolve) => setTimeout(resolve, 2000)) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + } + + throw err + } + }) + } + + it('should detect runtime errors on the module scope', async () => { + const browser = await next.browser(basePath + '/hmr/about3') + const aboutPage = join('pages', 'hmr', 'about3.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace('export', 'aa=20;\nexport') + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } finally { + await next.patchFile(aboutPage, aboutContent) + } + }) + + it('should recover from errors in the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about4') + const aboutPage = join('pages', 'hmr', 'about4.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'return', + 'throw new Error("an-expected-error");\nreturn' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after exporting an invalid page', async () => { + const browser = await next.browser(basePath + '/hmr/about5') + const aboutPage = join('pages', 'hmr', 'about5.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default {};\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about5""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after a bad return from the render function', async () => { + const browser = await next.browser(basePath + '/hmr/about6') + const aboutPage = join('pages', 'hmr', 'about6.js') + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default () => /search/;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + // TODO: Replace this when webpack 5 is the default + expect(await getRedboxHeader(browser)).toMatch( + `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after undefined exported as default', async () => { + const browser = await next.browser(basePath + '/hmr/about7') + const aboutPage = join('pages', 'hmr', 'about7.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'export default undefined;\nexport const fn =' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: The default export is not a React Component in page: "/hmr/about7""` + ) + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after webpack parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about8') + const aboutPage = join('pages', 'hmr', 'about8.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.xyz"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + + if (process.env.TURBOPACK) { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Unknown module type + This module doesn't have an associated type. Use a known file extension, or register a loader for it. + + Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders" + `) + } else { + expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + "./components/parse-error.xyz + Module parse failed: Unexpected token (3:0) + You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders + | This + | is + > }}} + | invalid + | js + + Import trace for requested module: + ./components/parse-error.xyz + ./pages/hmr/about8.js" + `) + } + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + throw err + } + }) + + it('should recover after loader parse error in an imported file', async () => { + const browser = await next.browser(basePath + '/hmr/about9') + const aboutPage = join('pages', 'hmr', 'about9.js') + + const aboutContent = await next.readFile(aboutPage) + try { + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + + await next.patchFile( + aboutPage, + aboutContent.replace( + 'export default', + 'import "../../components/parse-error.js"\nexport default' + ) + ) + + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') + let redboxSource = await getRedboxSource(browser) + + redboxSource = redboxSource.replace(`${next.testDir}`, '.') + if (process.env.TURBOPACK) { + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js:3:1 + Parsing ecmascript source code failed + 1 | This + 2 | is + > 3 | }}} + | ^ + 4 | invalid + 5 | js + + Expression expected" + `) + } else { + redboxSource = redboxSource.substring(0, redboxSource.indexOf('`----')) + + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` + "./components/parse-error.js + Error: x Expression expected + ,-[3:1] + 1 | This + 2 | is + 3 | }}} + : ^ + 4 | invalid + 5 | js + " + `) + } + + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + } + }) + + it('should recover from errors in getInitialProps in client', async () => { + const browser = await next.browser(basePath + '/hmr') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await browser.elementByCss('#error-in-gip-link').click() + + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + it('should recover after an error reported via SSR', async () => { + const browser = await next.browser(basePath + '/hmr/error-in-gip') + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + const errorContent = await next.readFile(erroredPage) + try { + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) + + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/Hello/) + }) + + await next.patchFile(erroredPage, errorContent) + + await retry(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') + } + return expect(await getRedboxSource(browser)).toMatch( + /an-expected-error-in-gip/ + ) + }) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } + }) + + if (!process.env.TURBOPACK) { + it('should have client HMR events in trace file', async () => { + const traceData = await next.readFile('.next/trace') + expect(traceData).toContain('client-hmr-latency') + expect(traceData).toContain('client-error') + expect(traceData).toContain('client-success') + expect(traceData).toContain('client-full-reload') + }) + } +}) diff --git a/test/development/basic/hmr/error-recovery.test.ts b/test/development/basic/hmr/error-recovery.test.ts deleted file mode 100644 index 1ffe29a6bb25c..0000000000000 --- a/test/development/basic/hmr/error-recovery.test.ts +++ /dev/null @@ -1,740 +0,0 @@ -import type { NextConfig } from 'next' -import { join } from 'path' -import { - assertHasRedbox, - assertNoRedbox, - getBrowserBodyText, - getRedboxHeader, - getRedboxDescription, - getRedboxSource, - renderViaHTTP, - retry, - waitFor, -} from 'next-test-utils' -import { nextTestSetup } from 'e2e-utils' -import { outdent } from 'outdent' - -describe.each([ - { basePath: '', assetPrefix: '' }, - { basePath: '', assetPrefix: '/asset-prefix' }, - { basePath: '/docs', assetPrefix: '' }, - { basePath: '/docs', assetPrefix: '/asset-prefix' }, -])( - 'HMR - Error Recovery, nextConfig: %o', - (nextConfig: Partial) => { - const { next } = nextTestSetup({ - files: __dirname, - nextConfig, - patchFileDelay: 500, - }) - const { basePath } = nextConfig - - it('should recover from 404 after a page has been added', async () => { - const newPage = join('pages', 'hmr', 'new-page.js') - - try { - const browser = await next.browser(basePath + '/hmr/new-page') - - expect(await browser.elementByCss('body').text()).toMatch( - /This page could not be found/ - ) - - // Add the page - await next.patchFile( - newPage, - 'export default () => (
the-new-page
)' - ) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) - }) - - await next.deleteFile(newPage) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This page could not be found/ - ) - }) - - expect(next.cliOutput).toContain('Compiled /_error') - } catch (err) { - await next.deleteFile(newPage) - throw err - } - }) - - it('should recover from 404 after a page has been added with dynamic segments', async () => { - const newPage = join('pages', 'hmr', '[foo]', 'page.js') - - try { - const browser = await next.browser(basePath + '/hmr/foo/page') - - expect(await browser.elementByCss('body').text()).toMatch( - /This page could not be found/ - ) - - // Add the page - await next.patchFile( - newPage, - 'export default () => (
the-new-page
)' - ) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) - }) - - await next.deleteFile(newPage) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This page could not be found/ - ) - }) - - expect(next.cliOutput).toContain('Compiled /_error') - } catch (err) { - await next.deleteFile(newPage) - throw err - } - }) - ;(process.env.TURBOPACK ? it.skip : it)( - // this test fails frequently with turbopack - 'should not continously poll a custom error page', - async () => { - const errorPage = join('pages', '_error.js') - - await next.patchFile( - errorPage, - outdent` - function Error({ statusCode, message, count }) { - return ( -
- Error Message: {message} -
- ) - } - - Error.getInitialProps = async ({ res, err }) => { - const statusCode = res ? res.statusCode : err ? err.statusCode : 404 - console.log('getInitialProps called'); - return { - statusCode, - message: err ? err.message : 'Oops...', - } - } - - export default Error - ` - ) - - try { - // navigate to a 404 page - await next.browser(basePath + '/does-not-exist') - - await retry(() => { - // eslint-disable-next-line jest/no-standalone-expect - expect(next.cliOutput).toMatch(/getInitialProps called/) - }) - - const outputIndex = next.cliOutput.length - - // wait a few seconds to ensure polling didn't happen - await waitFor(3000) - - const logOccurrences = - next.cliOutput.slice(outputIndex).split('getInitialProps called') - .length - 1 - // eslint-disable-next-line jest/no-standalone-expect - expect(logOccurrences).toBe(0) - } finally { - await next.deleteFile(errorPage) - } - } - ) - - it('should detect syntax errors and recover', async () => { - const browser = await next.browser(basePath + '/hmr/about2') - const aboutPage = join('pages', 'hmr', 'about2.js') - const aboutContent = await next.readFile(aboutPage) - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile(aboutPage, aboutContent.replace('', 'div')) - - await assertHasRedbox(browser) - const source = next.normalizeTestDirContent( - await getRedboxSource(browser) - ) - if (basePath === '' && !process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` - "./pages/hmr/about2.js - Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? - ,-[7:1] - 4 |

This is the about page.

- 5 | div - 6 | ) - 7 | } - : ^ - \`---- - x Unexpected eof - ,-[7:3] - 5 | div - 6 | ) - 7 | } - \`---- - - Caused by: - Syntax Error - - Import trace for requested module: - ./pages/hmr/about2.js" - `) - } else if (basePath === '' && process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` - "./pages/hmr/about2.js:7:1 - Parsing ecmascript source code failed - 5 | div - 6 | ) - > 7 | } - | ^ - 8 | - - Unexpected token. Did you mean \`{'}'}\` or \`}\`?" - `) - } else if (basePath === '/docs' && !process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` - "./pages/hmr/about2.js - Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? - ,-[7:1] - 4 |

This is the about page.

- 5 | div - 6 | ) - 7 | } - : ^ - \`---- - x Unexpected eof - ,-[7:3] - 5 | div - 6 | ) - 7 | } - \`---- - - Caused by: - Syntax Error - - Import trace for requested module: - ./pages/hmr/about2.js" - `) - } else if (basePath === '/docs' && process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` - "./pages/hmr/about2.js:7:1 - Parsing ecmascript source code failed - 5 | div - 6 | ) - > 7 | } - | ^ - 8 | - - Unexpected token. Did you mean \`{'}'}\` or \`}\`?" - `) - } - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - }) - - if (!process.env.TURBOPACK) { - // Turbopack doesn't have this restriction - it('should show the error on all pages', async () => { - const aboutPage = join('pages', 'hmr', 'about2.js') - const aboutContent = await next.readFile(aboutPage) - const browser = await next.browser(basePath + '/hmr/contact') - try { - await renderViaHTTP(next.url, basePath + '/hmr/about2') - - await next.patchFile(aboutPage, aboutContent.replace('', 'div')) - - // Ensure dev server has time to break: - await new Promise((resolve) => setTimeout(resolve, 2000)) - - await assertHasRedbox(browser) - expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the contact page/ - ) - }) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the contact page/ - ) - }) - } - - throw err - } - }) - } - - it('should detect runtime errors on the module scope', async () => { - const browser = await next.browser(basePath + '/hmr/about3') - const aboutPage = join('pages', 'hmr', 'about3.js') - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace('export', 'aa=20;\nexport') - ) - - await assertHasRedbox(browser) - expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } finally { - await next.patchFile(aboutPage, aboutContent) - } - }) - - it('should recover from errors in the render function', async () => { - const browser = await next.browser(basePath + '/hmr/about4') - const aboutPage = join('pages', 'hmr', 'about4.js') - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace( - 'return', - 'throw new Error("an-expected-error");\nreturn' - ) - ) - - await assertHasRedbox(browser) - expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - throw err - } - }) - - it('should recover after exporting an invalid page', async () => { - const browser = await next.browser(basePath + '/hmr/about5') - const aboutPage = join('pages', 'hmr', 'about5.js') - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default {};\nexport const fn =' - ) - ) - - await assertHasRedbox(browser) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: The default export is not a React Component in page: "/hmr/about5""` - ) - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - throw err - } - }) - - it('should recover after a bad return from the render function', async () => { - const browser = await next.browser(basePath + '/hmr/about6') - const aboutPage = join('pages', 'hmr', 'about6.js') - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default () => /search/;\nexport const fn =' - ) - ) - - await assertHasRedbox(browser) - // TODO: Replace this when webpack 5 is the default - expect(await getRedboxHeader(browser)).toMatch( - `Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.` - ) - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - throw err - } - }) - - it('should recover after undefined exported as default', async () => { - const browser = await next.browser(basePath + '/hmr/about7') - const aboutPage = join('pages', 'hmr', 'about7.js') - - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default undefined;\nexport const fn =' - ) - ) - - await assertHasRedbox(browser) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: The default export is not a React Component in page: "/hmr/about7""` - ) - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - await assertNoRedbox(browser) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - throw err - } - }) - - it('should recover after webpack parse error in an imported file', async () => { - const browser = await next.browser(basePath + '/hmr/about8') - const aboutPage = join('pages', 'hmr', 'about8.js') - - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'import "../../components/parse-error.xyz"\nexport default' - ) - ) - - await assertHasRedbox(browser) - expect(await getRedboxHeader(browser)).toMatch('Failed to compile') - - if (process.env.TURBOPACK) { - expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` - "./components/parse-error.xyz - Unknown module type - This module doesn't have an associated type. Use a known file extension, or register a loader for it. - - Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders" - `) - } else { - expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` - "./components/parse-error.xyz - Module parse failed: Unexpected token (3:0) - You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders - | This - | is - > }}} - | invalid - | js - - Import trace for requested module: - ./components/parse-error.xyz - ./pages/hmr/about8.js" - `) - } - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - await assertNoRedbox(browser) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - throw err - } - }) - - it('should recover after loader parse error in an imported file', async () => { - const browser = await next.browser(basePath + '/hmr/about9') - const aboutPage = join('pages', 'hmr', 'about9.js') - - const aboutContent = await next.readFile(aboutPage) - try { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'import "../../components/parse-error.js"\nexport default' - ) - ) - - await assertHasRedbox(browser) - expect(await getRedboxHeader(browser)).toMatch('Failed to compile') - let redboxSource = await getRedboxSource(browser) - - redboxSource = redboxSource.replace(`${next.testDir}`, '.') - if (process.env.TURBOPACK) { - expect(next.normalizeTestDirContent(redboxSource)) - .toMatchInlineSnapshot(` - "./components/parse-error.js:3:1 - Parsing ecmascript source code failed - 1 | This - 2 | is - > 3 | }}} - | ^ - 4 | invalid - 5 | js - - Expression expected" - `) - } else { - redboxSource = redboxSource.substring( - 0, - redboxSource.indexOf('`----') - ) - - expect(next.normalizeTestDirContent(redboxSource)) - .toMatchInlineSnapshot(` - "./components/parse-error.js - Error: x Expression expected - ,-[3:1] - 1 | This - 2 | is - 3 | }}} - : ^ - 4 | invalid - 5 | js - " - `) - } - - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - await assertNoRedbox(browser) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - }) - - it('should recover from errors in getInitialProps in client', async () => { - const browser = await next.browser(basePath + '/hmr') - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - const errorContent = await next.readFile(erroredPage) - try { - await browser.elementByCss('#error-in-gip-link').click() - - await assertHasRedbox(browser) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: an-expected-error-in-gip"` - ) - - await next.patchFile( - erroredPage, - errorContent.replace('throw error', 'return {}') - ) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch(/Hello/) - }) - - await next.patchFile(erroredPage, errorContent) - - await retry(async () => { - await browser.refresh() - await waitFor(2000) - const text = await getBrowserBodyText(browser) - if (text.includes('Hello')) { - throw new Error('waiting') - } - return expect(await getRedboxSource(browser)).toMatch( - /an-expected-error-in-gip/ - ) - }) - } catch (err) { - await next.patchFile(erroredPage, errorContent) - - throw err - } - }) - - it('should recover after an error reported via SSR', async () => { - const browser = await next.browser(basePath + '/hmr/error-in-gip') - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - const errorContent = await next.readFile(erroredPage) - try { - await assertHasRedbox(browser) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: an-expected-error-in-gip"` - ) - - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - - await next.patchFile( - erroredPage, - errorContent.replace('throw error', 'return {}') - ) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch(/Hello/) - }) - - await next.patchFile(erroredPage, errorContent) - - await retry(async () => { - await browser.refresh() - await waitFor(2000) - const text = await getBrowserBodyText(browser) - if (text.includes('Hello')) { - throw new Error('waiting') - } - return expect(await getRedboxSource(browser)).toMatch( - /an-expected-error-in-gip/ - ) - }) - } catch (err) { - await next.patchFile(erroredPage, errorContent) - - throw err - } - }) - - if (!process.env.TURBOPACK) { - it('should have client HMR events in trace file', async () => { - const traceData = await next.readFile('.next/trace') - expect(traceData).toContain('client-hmr-latency') - expect(traceData).toContain('client-error') - expect(traceData).toContain('client-success') - expect(traceData).toContain('client-full-reload') - }) - } - } -) diff --git a/test/development/basic/hmr/full-reload.test.ts b/test/development/basic/hmr/full-reload-no-base-path-no-asset-prefix.test.ts similarity index 89% rename from test/development/basic/hmr/full-reload.test.ts rename to test/development/basic/hmr/full-reload-no-base-path-no-asset-prefix.test.ts index adc5b088b73f8..c80477a58d21c 100644 --- a/test/development/basic/hmr/full-reload.test.ts +++ b/test/development/basic/hmr/full-reload-no-base-path-no-asset-prefix.test.ts @@ -1,15 +1,12 @@ -import type { NextConfig } from 'next' +import { join } from 'path' import { getRedboxHeader, retry } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' -describe.each([ - { basePath: '', assetPrefix: '' }, - { basePath: '', assetPrefix: '/asset-prefix' }, - { basePath: '/docs', assetPrefix: '' }, - { basePath: '/docs', assetPrefix: '/asset-prefix' }, -])('HMR - Full Reload, nextConfig: %o', (nextConfig: Partial) => { +const nextConfig = { basePath: '', assetPrefix: '' } + +describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => { const { next } = nextTestSetup({ - files: __dirname, + files: join(__dirname, '../../fixtures'), nextConfig, patchFileDelay: 500, }) diff --git a/test/development/basic/hmr/full-reload-no-base-path-yes-asset-prefix.test.ts b/test/development/basic/hmr/full-reload-no-base-path-yes-asset-prefix.test.ts new file mode 100644 index 0000000000000..61030488a19ac --- /dev/null +++ b/test/development/basic/hmr/full-reload-no-base-path-yes-asset-prefix.test.ts @@ -0,0 +1,93 @@ +import { join } from 'path' +import { getRedboxHeader, retry } from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' + +const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' } + +describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should warn about full reload in cli output - anonymous page function', async () => { + const start = next.cliOutput.length + const browser = await next.browser( + basePath + '/hmr/anonymous-page-function' + ) + const cliWarning = + 'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload' + + expect(await browser.elementByCss('p').text()).toBe('hello world') + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + + const currentFileContent = await next.readFile( + './pages/hmr/anonymous-page-function.js' + ) + const newFileContent = currentFileContent.replace( + '

hello world

', + '

hello world!!!

' + ) + await next.patchFile( + './pages/hmr/anonymous-page-function.js', + newFileContent + ) + + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'hello world!!!' + ) + + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." + ) + ) + ).toBeTruthy() + }) + + it('should warn about full reload in cli output - runtime-error', async () => { + const start = next.cliOutput.length + const browser = await next.browser(basePath + '/hmr/runtime-error') + const cliWarning = + 'Fast Refresh had to perform a full reload due to a runtime error.' + + await retry(async () => { + expect(await getRedboxHeader(browser)).toMatch( + /ReferenceError: whoops is not defined/ + ) + }) + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + + const currentFileContent = await next.readFile( + './pages/hmr/runtime-error.js' + ) + const newFileContent = currentFileContent.replace( + 'whoops', + '

whoops

' + ) + await next.patchFile('./pages/hmr/runtime-error.js', newFileContent) + + expect(await browser.waitForElementByCss('#updated').text()).toBe('whoops') + + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + '[Fast Refresh] performing full reload because your application had an unrecoverable error' + ) + ) + ).toBeTruthy() + }) +}) diff --git a/test/development/basic/hmr/full-reload-yes-base-path-no-asset-prefix.test.ts b/test/development/basic/hmr/full-reload-yes-base-path-no-asset-prefix.test.ts new file mode 100644 index 0000000000000..f596b78787133 --- /dev/null +++ b/test/development/basic/hmr/full-reload-yes-base-path-no-asset-prefix.test.ts @@ -0,0 +1,93 @@ +import { join } from 'path' +import { getRedboxHeader, retry } from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' + +const nextConfig = { basePath: '/docs', assetPrefix: '' } + +describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should warn about full reload in cli output - anonymous page function', async () => { + const start = next.cliOutput.length + const browser = await next.browser( + basePath + '/hmr/anonymous-page-function' + ) + const cliWarning = + 'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload' + + expect(await browser.elementByCss('p').text()).toBe('hello world') + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + + const currentFileContent = await next.readFile( + './pages/hmr/anonymous-page-function.js' + ) + const newFileContent = currentFileContent.replace( + '

hello world

', + '

hello world!!!

' + ) + await next.patchFile( + './pages/hmr/anonymous-page-function.js', + newFileContent + ) + + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'hello world!!!' + ) + + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." + ) + ) + ).toBeTruthy() + }) + + it('should warn about full reload in cli output - runtime-error', async () => { + const start = next.cliOutput.length + const browser = await next.browser(basePath + '/hmr/runtime-error') + const cliWarning = + 'Fast Refresh had to perform a full reload due to a runtime error.' + + await retry(async () => { + expect(await getRedboxHeader(browser)).toMatch( + /ReferenceError: whoops is not defined/ + ) + }) + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + + const currentFileContent = await next.readFile( + './pages/hmr/runtime-error.js' + ) + const newFileContent = currentFileContent.replace( + 'whoops', + '

whoops

' + ) + await next.patchFile('./pages/hmr/runtime-error.js', newFileContent) + + expect(await browser.waitForElementByCss('#updated').text()).toBe('whoops') + + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + '[Fast Refresh] performing full reload because your application had an unrecoverable error' + ) + ) + ).toBeTruthy() + }) +}) diff --git a/test/development/basic/hmr/full-reload-yes-base-path-yes-asset-prefix.test.ts b/test/development/basic/hmr/full-reload-yes-base-path-yes-asset-prefix.test.ts new file mode 100644 index 0000000000000..d76646c8f0bfc --- /dev/null +++ b/test/development/basic/hmr/full-reload-yes-base-path-yes-asset-prefix.test.ts @@ -0,0 +1,93 @@ +import { join } from 'path' +import { getRedboxHeader, retry } from 'next-test-utils' +import { nextTestSetup } from 'e2e-utils' + +const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' } + +describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => { + const { next } = nextTestSetup({ + files: join(__dirname, '../../fixtures'), + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + it('should warn about full reload in cli output - anonymous page function', async () => { + const start = next.cliOutput.length + const browser = await next.browser( + basePath + '/hmr/anonymous-page-function' + ) + const cliWarning = + 'Fast Refresh had to perform a full reload when ./pages/hmr/anonymous-page-function.js changed. Read more: https://nextjs.org/docs/messages/fast-refresh-reload' + + expect(await browser.elementByCss('p').text()).toBe('hello world') + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + + const currentFileContent = await next.readFile( + './pages/hmr/anonymous-page-function.js' + ) + const newFileContent = currentFileContent.replace( + '

hello world

', + '

hello world!!!

' + ) + await next.patchFile( + './pages/hmr/anonymous-page-function.js', + newFileContent + ) + + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'hello world!!!' + ) + + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + "Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree." + ) + ) + ).toBeTruthy() + }) + + it('should warn about full reload in cli output - runtime-error', async () => { + const start = next.cliOutput.length + const browser = await next.browser(basePath + '/hmr/runtime-error') + const cliWarning = + 'Fast Refresh had to perform a full reload due to a runtime error.' + + await retry(async () => { + expect(await getRedboxHeader(browser)).toMatch( + /ReferenceError: whoops is not defined/ + ) + }) + expect(next.cliOutput.slice(start)).not.toContain(cliWarning) + + const currentFileContent = await next.readFile( + './pages/hmr/runtime-error.js' + ) + const newFileContent = currentFileContent.replace( + 'whoops', + '

whoops

' + ) + await next.patchFile('./pages/hmr/runtime-error.js', newFileContent) + + expect(await browser.waitForElementByCss('#updated').text()).toBe('whoops') + + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) + + // Browser warning + const browserLogs = await browser.log() + expect( + browserLogs.some(({ message }) => + message.includes( + '[Fast Refresh] performing full reload because your application had an unrecoverable error' + ) + ) + ).toBeTruthy() + }) +}) diff --git a/test/development/basic/hmr/hot-module-reload.test.ts b/test/development/basic/hmr/hot-module-reload/hot-module-reload.test.ts similarity index 99% rename from test/development/basic/hmr/hot-module-reload.test.ts rename to test/development/basic/hmr/hot-module-reload/hot-module-reload.test.ts index 1b284ea0d5c1e..d5267aca548a7 100644 --- a/test/development/basic/hmr/hot-module-reload.test.ts +++ b/test/development/basic/hmr/hot-module-reload/hot-module-reload.test.ts @@ -18,7 +18,7 @@ describe.each([ 'HMR - Hot Module Reload, nextConfig: %o', (nextConfig: Partial) => { const { next } = nextTestSetup({ - files: __dirname, + files: join(__dirname, '../../fixtures'), nextConfig, patchFileDelay: 500, }) diff --git a/test/development/basic/hmr/pages/auto-export-is-ready.js b/test/development/basic/hmr/pages/auto-export-is-ready.js deleted file mode 100644 index ced7f78a516e9..0000000000000 --- a/test/development/basic/hmr/pages/auto-export-is-ready.js +++ /dev/null @@ -1,12 +0,0 @@ -import { useRouter } from 'next/router' - -export default function Page(props) { - const router = useRouter() - return ( - <> -

auto-export router.isReady

-

{JSON.stringify(router.query)}

-

{router.isReady ? 'yes' : 'no'}

- - ) -} diff --git a/test/development/basic/hmr/pages/gsp-is-ready.js b/test/development/basic/hmr/pages/gsp-is-ready.js deleted file mode 100644 index 03912605ff039..0000000000000 --- a/test/development/basic/hmr/pages/gsp-is-ready.js +++ /dev/null @@ -1,20 +0,0 @@ -import { useRouter } from 'next/router' - -export default function Page(props) { - const router = useRouter() - return ( - <> -

getStaticProps router.isReady

-

{JSON.stringify(router.query)}

-

{router.isReady ? 'yes' : 'no'}

- - ) -} - -export function getStaticProps() { - return { - props: { - now: Date.now(), - }, - } -} diff --git a/test/development/basic/hmr/pages/hmr/about.js b/test/development/basic/hmr/pages/hmr/about.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about1.js b/test/development/basic/hmr/pages/hmr/about1.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about1.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about2.js b/test/development/basic/hmr/pages/hmr/about2.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about2.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about3.js b/test/development/basic/hmr/pages/hmr/about3.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about3.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about4.js b/test/development/basic/hmr/pages/hmr/about4.js deleted file mode 100644 index dc234f13d391f..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about4.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function About4() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about5.js b/test/development/basic/hmr/pages/hmr/about5.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about5.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about6.js b/test/development/basic/hmr/pages/hmr/about6.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about6.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about7.js b/test/development/basic/hmr/pages/hmr/about7.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about7.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about8.js b/test/development/basic/hmr/pages/hmr/about8.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about8.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/about9.js b/test/development/basic/hmr/pages/hmr/about9.js deleted file mode 100644 index aad74adb6f7a3..0000000000000 --- a/test/development/basic/hmr/pages/hmr/about9.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the about page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/anonymous-page-function.js b/test/development/basic/hmr/pages/hmr/anonymous-page-function.js deleted file mode 100644 index 10e018391ed83..0000000000000 --- a/test/development/basic/hmr/pages/hmr/anonymous-page-function.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function () { - return

hello world

-} diff --git a/test/development/basic/hmr/pages/hmr/contact.js b/test/development/basic/hmr/pages/hmr/contact.js deleted file mode 100644 index 10f65cfdf71b8..0000000000000 --- a/test/development/basic/hmr/pages/hmr/contact.js +++ /dev/null @@ -1,7 +0,0 @@ -export default function Page() { - return ( -
-

This is the contact page.

-
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/counter.js b/test/development/basic/hmr/pages/hmr/counter.js deleted file mode 100644 index 53cb27eaf1780..0000000000000 --- a/test/development/basic/hmr/pages/hmr/counter.js +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react' - -export default class Counter extends React.Component { - state = { count: 0 } - - incr() { - const { count } = this.state - this.setState({ count: count + 1 }) - } - - render() { - return ( -
-

COUNT: {this.state.count}

- -
- ) - } -} diff --git a/test/development/basic/hmr/pages/hmr/error-in-gip.js b/test/development/basic/hmr/pages/hmr/error-in-gip.js deleted file mode 100644 index 66ee7e1abdef2..0000000000000 --- a/test/development/basic/hmr/pages/hmr/error-in-gip.js +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react' -export default class Page extends React.Component { - static getInitialProps() { - const error = new Error('an-expected-error-in-gip') - throw error - } - - render() { - return
Hello
- } -} diff --git a/test/development/basic/hmr/pages/hmr/index.js b/test/development/basic/hmr/pages/hmr/index.js deleted file mode 100644 index 9a753cead1a18..0000000000000 --- a/test/development/basic/hmr/pages/hmr/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import Link from 'next/link' - -export default function Page() { - return ( -
- - Bad Page - -
- ) -} diff --git a/test/development/basic/hmr/pages/hmr/nonlatin.js b/test/development/basic/hmr/pages/hmr/nonlatin.js deleted file mode 100644 index 3edaae9792cbd..0000000000000 --- a/test/development/basic/hmr/pages/hmr/nonlatin.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function NonLatin(props) { - return
ใƒ†ใ‚นใƒˆ
-} diff --git a/test/development/basic/hmr/pages/hmr/runtime-error.js b/test/development/basic/hmr/pages/hmr/runtime-error.js deleted file mode 100644 index 8ee8aeb6ea45c..0000000000000 --- a/test/development/basic/hmr/pages/hmr/runtime-error.js +++ /dev/null @@ -1,4 +0,0 @@ -export default function () { - // eslint-disable-next-line no-undef - return whoops -} diff --git a/test/development/basic/hmr/pages/hmr/style-dynamic-component.js b/test/development/basic/hmr/pages/hmr/style-dynamic-component.js deleted file mode 100644 index 6f3567b4c55b4..0000000000000 --- a/test/development/basic/hmr/pages/hmr/style-dynamic-component.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react' -import dynamic from 'next/dynamic' - -const HmrDynamic = dynamic(import('../../components/hmr/dynamic')) - -export default function Page() { - return -} diff --git a/test/development/basic/hmr/pages/hmr/style-stateful-component.js b/test/development/basic/hmr/pages/hmr/style-stateful-component.js deleted file mode 100644 index 47210f1868cda..0000000000000 --- a/test/development/basic/hmr/pages/hmr/style-stateful-component.js +++ /dev/null @@ -1,20 +0,0 @@ -import React, { Component } from 'react' - -export default class StyleStateFul extends Component { - render() { - return ( - -
-

- This is the style page. - -

-
-
- ) - } -} diff --git a/test/development/basic/hmr/pages/hmr/style.js b/test/development/basic/hmr/pages/hmr/style.js deleted file mode 100644 index 4c85f791336b9..0000000000000 --- a/test/development/basic/hmr/pages/hmr/style.js +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react' -export default function Style() { - return ( - -
-

- This is the style page. - -

-
-
- ) -} diff --git a/test/development/basic/hmr/pages/hydration-error.js b/test/development/basic/hmr/pages/hydration-error.js deleted file mode 100644 index 46ffdde96a2fb..0000000000000 --- a/test/development/basic/hmr/pages/hydration-error.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function Page(props) { - return

is server {typeof window}

-}