import { join } from 'path' import { waitForRedbox, waitForNoRedbox, getBrowserBodyText, getRedboxHeader, getRedboxDescription, getRedboxSource, retry, waitFor, trimEndMultiline, getDistDir, } from 'next-test-utils' import { nextTestSetup } from 'e2e-utils' import { outdent } from 'outdent' export function runErrorRecoveryHmrTest(nextConfig: { basePath: string assetPrefix: string }) { const { next } = nextTestSetup({ files: __dirname, nextConfig, patchFileDelay: 500, }) const { basePath } = nextConfig it('should recover from 404 after a page has been added', async () => { const browser = await next.browser(basePath + '/hmr/new-page') expect(await browser.elementByCss('body').text()).toMatch( /This page could not be found/ ) expect(next.cliOutput).toContain('GET /hmr/new-page 404') let cliOutputLength = next.cliOutput.length // Add the page await next.patchFile( join('pages', 'hmr', 'new-page.js'), 'export default () => (
the-new-page
)', async () => { await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) }) expect(next.cliOutput.slice(cliOutputLength)).toContain( 'GET /hmr/new-page 200' ) cliOutputLength = next.cliOutput.length } ) // page was deleted at the end of patchFile await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This page could not be found/ ) }) expect(next.cliOutput.slice(cliOutputLength)).toContain( 'GET /hmr/new-page 404' ) }) it('should recover from 404 after a page has been added with dynamic segments', async () => { const browser = await next.browser(basePath + '/hmr/foo/page') expect(await browser.elementByCss('body').text()).toMatch( /This page could not be found/ ) expect(next.cliOutput).toContain('GET /hmr/foo/page 404') let cliOutputLength = next.cliOutput.length // Add the page await next.patchFile( join('pages', 'hmr', '[foo]', 'page.js'), 'export default () => (
the-new-page
)', async () => { await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/) }) expect(next.cliOutput.slice(cliOutputLength)).toContain( 'GET /hmr/foo/page 200' ) cliOutputLength = next.cliOutput.length } ) // page was deleted at the end of patchFile await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This page could not be found/ ) }) expect(next.cliOutput.slice(cliOutputLength)).toContain( 'GET /hmr/foo/page 404' ) }) ;(process.env.IS_TURBOPACK_TEST ? it.skip : it)( // this test fails frequently with turbopack 'should not continously poll a custom error page', async () => { await next.patchFile( join('pages', '_error.js'), 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 `, async () => { // navigate to a 404 page await next.browser(basePath + '/does-not-exist') await retry(() => { 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 expect(logOccurrences).toBe(0) } ) } ) it('should detect syntax errors and recover', async () => { const browser = await next.browser(basePath + '/hmr/about2') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about2.js'), (content) => content.replace('', 'div'), async () => { await waitForRedbox(browser) const source = next.normalizeTestDirContent( await getRedboxSource(browser) ) if (process.env.IS_TURBOPACK_TEST) { expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js (7:1) Unexpected token. Did you mean \`{'}'}\` or \`}\`? 5 | div 6 | ) > 7 | } | ^ 8 | Parsing ecmascript source code failed" `) } else if (process.env.NEXT_RSPACK) { expect(trimEndMultiline(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 Expected '' │ ,-[7:3] │ 5 | div │ 6 | ) │ 7 | } │ \`---- │ │ │ Caused by: │ Syntax Error Import trace for requested module: ./pages/hmr/about2.js" `) } else { 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 Expected '' ,-[7:3] 5 | div 6 | ) 7 | } \`---- Caused by: Syntax Error Import trace for requested module: ./pages/hmr/about2.js" `) } } ) await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) }) if (!process.env.IS_TURBOPACK_TEST) { // Turbopack doesn't have this restriction it('should show the error on all pages', async () => { const browser = await next.browser(basePath + '/hmr/contact') await next.render(basePath + '/hmr/about2') await next.patchFile( join('pages', 'hmr', 'about2.js'), (content) => content.replace('', 'div'), async () => { // Ensure dev server has time to break: await new Promise((resolve) => setTimeout(resolve, 2000)) await waitForRedbox(browser) expect(await getRedboxSource(browser)).toContain( "Expected ''" ) } ) await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the contact page/ ) }) }) } it('should detect runtime errors on the module scope', async () => { const browser = await next.browser(basePath + '/hmr/about3') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about3.js'), (content) => content.replace('export', 'aa=20;\nexport'), async () => { await waitForRedbox(browser) expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) } ) await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) }) it('should recover from errors in the render function', async () => { const browser = await next.browser(basePath + '/hmr/about4') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about4.js'), (content) => content.replace( 'return', 'throw new Error("an-expected-error");\nreturn' ), async () => { await waitForRedbox(browser) expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) } ) await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) }) it('should recover after exporting an invalid page', async () => { const browser = await next.browser(basePath + '/hmr/about5') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about5.js'), (content) => content.replace( 'export default', 'export default {};\nexport const fn =' ), async () => { await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( `"The default export is not a React Component in page: "/hmr/about5""` ) } ) await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) }) it('should recover after a bad return from the render function', async () => { const browser = await next.browser(basePath + '/hmr/about6') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about6.js'), (content) => content.replace( 'export default', 'export default () => /search/;\nexport const fn =' ), async () => { await waitForRedbox(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 retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) }) 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) 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 =' ), async () => { await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( `"The default export is not a React Component in page: "/hmr/about7""` ) } ) await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await waitForNoRedbox(browser) }) it('should recover after webpack parse error in an imported file', async () => { const browser = await next.browser(basePath + '/hmr/about8') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about8.js'), (content) => content.replace( 'export default', 'import "../../components/parse-error.xyz"\nexport default' ), async () => { await waitForRedbox(browser) expect(await getRedboxHeader(browser)).toMatch('Build Error') if (process.env.IS_TURBOPACK_TEST) { 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 if (process.env.NEXT_RSPACK) { expect(trimEndMultiline(await getRedboxSource(browser))) .toMatchInlineSnapshot(` "./components/parse-error.xyz × Module parse failed: ╰─▶ × JavaScript parse error: Expression expected ╭─[3:0] 1 │ This 2 │ is 3 │ }}} · ─ 4 │ invalid 5 │ js ╰──── help: You may need an appropriate loader to handle this file type. Import trace for requested module: ./components/parse-error.xyz ./pages/hmr/about8.js" `) } 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 retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await waitForNoRedbox(browser) }) it('should recover after loader parse error in an imported file', async () => { const browser = await next.browser(basePath + '/hmr/about9') await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await next.patchFile( join('pages', 'hmr', 'about9.js'), (content) => content.replace( 'export default', 'import "../../components/parse-error.js"\nexport default' ), async () => { await waitForRedbox(browser) expect(await getRedboxHeader(browser)).toMatch('Build Error') let redboxSource = await getRedboxSource(browser) redboxSource = redboxSource.replace(`${next.testDir}`, '.') if (process.env.IS_TURBOPACK_TEST) { expect(next.normalizeTestDirContent(redboxSource)) .toMatchInlineSnapshot(` "./components/parse-error.js (3:1) Expression expected 1 | This 2 | is > 3 | }}} | ^ 4 | invalid 5 | js Parsing ecmascript source code failed Import traces: Browser: ./components/parse-error.js ./pages/hmr/about9.js SSR: ./components/parse-error.js ./pages/hmr/about9.js" `) } else if (process.env.NEXT_RSPACK) { expect(trimEndMultiline(next.normalizeTestDirContent(redboxSource))) .toMatchInlineSnapshot(` "./components/parse-error.js ╰─▶ × Error: x Expression expected │ ,-[3:1] │ 1 | This │ 2 | is │ 3 | }}} │ : ^ │ 4 | invalid │ 5 | js │ \`---- │ │ │ Caused by: │ Syntax Error Import trace for requested module: ./components/parse-error.js ./pages/hmr/about9.js" `) } 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 retry(async () => { expect(await getBrowserBodyText(browser)).toMatch( /This is the about page/ ) }) await waitForNoRedbox(browser) }) 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) await browser.elementByCss('#error-in-gip-link').click() await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( `"an-expected-error-in-gip"` ) await next.patchFile( erroredPage, (content) => content.replace('throw error', 'return {}'), async () => { 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/ ) }) } ) }) it('should recover after an error reported via SSR', async () => { const browser = await next.browser(basePath + '/hmr/error-in-gip') await waitForRedbox(browser) expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( `"an-expected-error-in-gip"` ) await next.patchFile( join('pages', 'hmr', 'error-in-gip.js'), (content) => content.replace('throw error', 'return {}'), async () => { await retry(async () => { expect(await getBrowserBodyText(browser)).toMatch(/Hello/) }) } ) 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/ ) }) }) if (!process.env.IS_TURBOPACK_TEST) { it('should have client HMR events in trace file', async () => { const traceData = await next.readFile(`${getDistDir()}/trace`) expect(traceData).toContain('client-hmr-latency') expect(traceData).toContain('client-error') expect(traceData).toContain('client-success') expect(traceData).toContain('client-full-reload') }) } }