From bec19858582fa4c4326379134d3694077b601a4e Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Sat, 7 Dec 2024 01:47:25 +0900 Subject: [PATCH] test: move HMR main test to a separate file --- test/development/basic/hmr.test.ts | 1094 ----------------- .../basic/hmr/hot-module-reload.test.ts | 282 +++++ 2 files changed, 282 insertions(+), 1094 deletions(-) create mode 100644 test/development/basic/hmr/hot-module-reload.test.ts diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index ca8b5010f5f87..e83a768b19025 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -1,20 +1,13 @@ import { join } from 'path' -import cheerio from 'cheerio' import webdriver from 'next-webdriver' import { assertHasRedbox, - assertNoRedbox, getBrowserBodyText, - getRedboxHeader, - getRedboxDescription, - getRedboxSource, - renderViaHTTP, retry, waitFor, } from 'next-test-utils' import { createNext } from 'e2e-utils' import { NextInstance } from 'e2e-utils' -import { outdent } from 'outdent' import type { NextConfig } from 'next' describe.each([ @@ -70,1093 +63,6 @@ describe.each([ }) }) - describe('Hot Module Reloading', () => { - describe('delete a page and add it back', () => { - it('should load the page properly', async () => { - const contactPagePath = join('pages', 'hmr', 'contact.js') - const newContactPagePath = join('pages', 'hmr', '_contact.js') - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/contact') - const text = await browser.elementByCss('p').text() - expect(text).toBe('This is the contact page.') - - // Rename the file to mimic a deleted page - await next.renameFile(contactPagePath, newContactPagePath) - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This page could not be found/ - ) - }) - - // Rename the file back to the original filename - await next.renameFile(newContactPagePath, contactPagePath) - - // wait until the page comes back - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the contact page/ - ) - }) - - expect(next.cliOutput).toContain('Compiled /_error') - } finally { - if (browser) { - await browser.close() - } - await next - .renameFile(newContactPagePath, contactPagePath) - .catch(() => {}) - } - }) - }) - - describe('editing a page', () => { - it('should detect the changes and display it', async () => { - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/about') - const text = await browser.elementByCss('p').text() - expect(text).toBe('This is the about page.') - - const aboutPagePath = join('pages', 'hmr', 'about.js') - - const originalContent = await next.readFile(aboutPagePath) - const editedContent = originalContent.replace( - 'This is the about page', - 'COOL page' - ) - - // change the content - try { - await next.patchFile(aboutPagePath, editedContent) - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch(/COOL page/) - }) - } finally { - // add the original content - await next.patchFile(aboutPagePath, originalContent) - } - - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should not reload unrelated pages', async () => { - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/counter') - const text = await browser - .elementByCss('button') - .click() - .elementByCss('button') - .click() - .elementByCss('p') - .text() - expect(text).toBe('COUNT: 2') - - const aboutPagePath = join('pages', 'hmr', 'about.js') - - const originalContent = await next.readFile(aboutPagePath) - const editedContent = originalContent.replace( - 'This is the about page', - 'COOL page' - ) - - try { - // Change the about.js page - await next.patchFile(aboutPagePath, editedContent) - - // Check whether the this page has reloaded or not. - await retry(async () => { - expect(await browser.elementByCss('p').text()).toMatch(/COUNT: 2/) - }) - } finally { - // restore the about page content. - await next.patchFile(aboutPagePath, originalContent) - } - } finally { - if (browser) { - await browser.close() - } - } - }) - - // Added because of a regression in react-hot-loader, see issues: #4246 #4273 - // Also: https://github.com/vercel/styled-jsx/issues/425 - it('should update styles correctly', async () => { - let browser - try { - browser = await webdriver(next.url, basePath + '/hmr/style') - const pTag = await browser.elementByCss('.hmr-style-page p') - const initialFontSize = await pTag.getComputedCss('font-size') - - expect(initialFontSize).toBe('100px') - - const pagePath = join('pages', 'hmr', 'style.js') - - const originalContent = await next.readFile(pagePath) - const editedContent = originalContent.replace('100px', '200px') - - // Change the page - await next.patchFile(pagePath, editedContent) - - try { - // Check whether the this page has reloaded or not. - await retry(async () => { - const editedPTag = await browser.elementByCss('.hmr-style-page p') - expect(await editedPTag.getComputedCss('font-size')).toBe('200px') - }) - } finally { - // Finally is used so that we revert the content back to the original regardless of the test outcome - // restore the about page content. - await next.patchFile(pagePath, originalContent) - } - } finally { - if (browser) { - await browser.close() - } - } - }) - - // Added because of a regression in react-hot-loader, see issues: #4246 #4273 - // Also: https://github.com/vercel/styled-jsx/issues/425 - it('should update styles in a stateful component correctly', async () => { - let browser - const pagePath = join('pages', 'hmr', 'style-stateful-component.js') - const originalContent = await next.readFile(pagePath) - try { - browser = await webdriver( - next.url, - basePath + '/hmr/style-stateful-component' - ) - const pTag = await browser.elementByCss('.hmr-style-page p') - const initialFontSize = await pTag.getComputedCss('font-size') - - expect(initialFontSize).toBe('100px') - const editedContent = originalContent.replace('100px', '200px') - - // Change the page - await next.patchFile(pagePath, editedContent) - - // Check whether the this page has reloaded or not. - await retry(async () => { - const editedPTag = await browser.elementByCss('.hmr-style-page p') - expect(await editedPTag.getComputedCss('font-size')).toBe('200px') - }) - } finally { - if (browser) { - await browser.close() - } - await next.patchFile(pagePath, originalContent) - } - }) - - // Added because of a regression in react-hot-loader, see issues: #4246 #4273 - // Also: https://github.com/vercel/styled-jsx/issues/425 - it('should update styles in a dynamic component correctly', async () => { - let browser = null - let secondBrowser = null - const pagePath = join('components', 'hmr', 'dynamic.js') - const originalContent = await next.readFile(pagePath) - try { - browser = await webdriver( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - const div = await browser.elementByCss('#dynamic-component') - const initialClientClassName = await div.getAttribute('class') - const initialFontSize = await div.getComputedCss('font-size') - - expect(initialFontSize).toBe('100px') - - const initialHtml = await renderViaHTTP( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - expect(initialHtml.includes('100px')).toBeTruthy() - - const $initialHtml = cheerio.load(initialHtml) - const initialServerClassName = - $initialHtml('#dynamic-component').attr('class') - - expect(initialClientClassName === initialServerClassName).toBeTruthy() - - const editedContent = originalContent.replace('100px', '200px') - - // Change the page - await next.patchFile(pagePath, editedContent) - - // wait for 5 seconds - await waitFor(5000) - - secondBrowser = await webdriver( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - // Check whether the this page has reloaded or not. - const editedDiv = - await secondBrowser.elementByCss('#dynamic-component') - const editedClientClassName = await editedDiv.getAttribute('class') - const editedFontSize = await editedDiv.getComputedCss('font-size') - const browserHtml = await secondBrowser.eval( - 'document.documentElement.innerHTML' - ) - - expect(editedFontSize).toBe('200px') - expect(browserHtml.includes('font-size:200px')).toBe(true) - expect(browserHtml.includes('font-size:100px')).toBe(false) - - const editedHtml = await renderViaHTTP( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - expect(editedHtml.includes('200px')).toBeTruthy() - const $editedHtml = cheerio.load(editedHtml) - const editedServerClassName = - $editedHtml('#dynamic-component').attr('class') - - expect(editedClientClassName === editedServerClassName).toBe(true) - } finally { - // Finally is used so that we revert the content back to the original regardless of the test outcome - // restore the about page content. - await next.patchFile(pagePath, originalContent) - - if (browser) { - await browser.close() - } - - if (secondBrowser) { - secondBrowser.close() - } - } - }) - - it('should not full reload when nonlatin characters are used', async () => { - let browser = null - const pagePath = join('pages', 'hmr', 'nonlatin.js') - const originalContent = await next.readFile(pagePath) - try { - browser = await webdriver(next.url, basePath + '/hmr/nonlatin') - const timeOrigin = await browser.eval('performance.timeOrigin') - const editedContent = originalContent.replace( - '
テスト
', - '
テスト
' - ) - - // Change the page - await next.patchFile(pagePath, editedContent) - - await browser.waitForElementByCss('.updated') - - expect(await browser.eval('performance.timeOrigin')).toEqual( - timeOrigin - ) - } finally { - // Finally is used so that we revert the content back to the original regardless of the test outcome - // restore the about page content. - await next.patchFile(pagePath, originalContent) - - if (browser) { - await browser.close() - } - } - }) - }) - }) - - describe('Error Recovery', () => { - it('should recover from 404 after a page has been added', async () => { - let browser - const newPage = join('pages', 'hmr', 'new-page.js') - - try { - browser = await webdriver(next.url, 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 - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover from 404 after a page has been added with dynamic segments', async () => { - let browser - const newPage = join('pages', 'hmr', '[foo]', 'page.js') - - try { - browser = await webdriver(next.url, 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 - } finally { - if (browser) { - await browser.close() - } - } - }) - ;(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 webdriver(next.url, 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 webdriver(next.url, 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) - let browser - 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)) - - browser = await webdriver(next.url, basePath + '/hmr/contact') - - 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 - } finally { - if (browser) { - await browser.close() - } - } - }) - } - - it('should detect runtime errors on the module scope', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about3.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about3') - 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) - if (browser) { - await browser.close() - } - } - }) - - it('should recover from errors in the render function', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about4.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about4') - 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) - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - - throw err - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover after exporting an invalid page', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about5.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about5') - 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) - - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - - throw err - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover after a bad return from the render function', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about6.js') - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about6') - 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) - - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - - throw err - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover after undefined exported as default', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about7.js') - - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/about7') - 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) - - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - - throw err - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover after webpack parse error in an imported file', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about8.js') - - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.appPort, basePath + '/hmr/about8') - 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) - - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - - throw err - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover after loader parse error in an imported file', async () => { - let browser - const aboutPage = join('pages', 'hmr', 'about9.js') - - const aboutContent = await next.readFile(aboutPage) - try { - browser = await webdriver(next.appPort, basePath + '/hmr/about9') - 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) - - if (browser) { - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) - } - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover from errors in getInitialProps in client', async () => { - let browser - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - const errorContent = await next.readFile(erroredPage) - try { - browser = await webdriver(next.url, basePath + '/hmr') - 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 - } finally { - if (browser) { - await browser.close() - } - } - }) - - it('should recover after an error reported via SSR', async () => { - let browser - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - const errorContent = await next.readFile(erroredPage) - try { - browser = await webdriver(next.url, basePath + '/hmr/error-in-gip') - - 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 - } finally { - if (browser) { - await browser.close() - } - } - }) - }) - - 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') - }) - } - it('should have correct compile timing after fixing error', async () => { const pageName = 'pages/auto-export-is-ready.js' const originalContent = await next.readFile(pageName) diff --git a/test/development/basic/hmr/hot-module-reload.test.ts b/test/development/basic/hmr/hot-module-reload.test.ts new file mode 100644 index 0000000000000..1b284ea0d5c1e --- /dev/null +++ b/test/development/basic/hmr/hot-module-reload.test.ts @@ -0,0 +1,282 @@ +import { join } from 'path' +import cheerio from 'cheerio' +import { + getBrowserBodyText, + renderViaHTTP, + retry, + waitFor, +} from 'next-test-utils' +import { 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' }, +])( + 'HMR - Hot Module Reload, nextConfig: %o', + (nextConfig: Partial) => { + const { next } = nextTestSetup({ + files: __dirname, + nextConfig, + patchFileDelay: 500, + }) + const { basePath } = nextConfig + + describe('delete a page and add it back', () => { + it('should load the page properly', async () => { + const contactPagePath = join('pages', 'hmr', 'contact.js') + const newContactPagePath = join('pages', 'hmr', '_contact.js') + const browser = await next.browser(basePath + '/hmr/contact') + try { + const text = await browser.elementByCss('p').text() + expect(text).toBe('This is the contact page.') + + // Rename the file to mimic a deleted page + await next.renameFile(contactPagePath, newContactPagePath) + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This page could not be found/ + ) + }) + + // Rename the file back to the original filename + await next.renameFile(newContactPagePath, contactPagePath) + + // wait until the page comes back + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the contact page/ + ) + }) + + expect(next.cliOutput).toContain('Compiled /_error') + } finally { + await next + .renameFile(newContactPagePath, contactPagePath) + .catch(() => {}) + } + }) + }) + + describe('editing a page', () => { + it('should detect the changes and display it', async () => { + const browser = await next.browser(basePath + '/hmr/about') + const text = await browser.elementByCss('p').text() + expect(text).toBe('This is the about page.') + + const aboutPagePath = join('pages', 'hmr', 'about.js') + + const originalContent = await next.readFile(aboutPagePath) + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) + + // change the content + try { + await next.patchFile(aboutPagePath, editedContent) + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch(/COOL page/) + }) + } finally { + // add the original content + await next.patchFile(aboutPagePath, originalContent) + } + + await retry(async () => { + expect(await getBrowserBodyText(browser)).toMatch( + /This is the about page/ + ) + }) + }) + + it('should not reload unrelated pages', async () => { + const browser = await next.browser(basePath + '/hmr/counter') + const text = await browser + .elementByCss('button') + .click() + .elementByCss('button') + .click() + .elementByCss('p') + .text() + expect(text).toBe('COUNT: 2') + + const aboutPagePath = join('pages', 'hmr', 'about.js') + + const originalContent = await next.readFile(aboutPagePath) + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) + + try { + // Change the about.js page + await next.patchFile(aboutPagePath, editedContent) + + // Check whether the this page has reloaded or not. + await retry(async () => { + expect(await browser.elementByCss('p').text()).toMatch(/COUNT: 2/) + }) + } finally { + // restore the about page content. + await next.patchFile(aboutPagePath, originalContent) + } + }) + + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/vercel/styled-jsx/issues/425 + it('should update styles correctly', async () => { + const browser = await next.browser(basePath + '/hmr/style') + const pTag = await browser.elementByCss('.hmr-style-page p') + const initialFontSize = await pTag.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') + + const pagePath = join('pages', 'hmr', 'style.js') + + const originalContent = await next.readFile(pagePath) + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + await next.patchFile(pagePath, editedContent) + + try { + // Check whether the this page has reloaded or not. + await retry(async () => { + const editedPTag = await browser.elementByCss('.hmr-style-page p') + expect(await editedPTag.getComputedCss('font-size')).toBe('200px') + }) + } finally { + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. + await next.patchFile(pagePath, originalContent) + } + }) + + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/vercel/styled-jsx/issues/425 + it('should update styles in a stateful component correctly', async () => { + const browser = await next.browser( + basePath + '/hmr/style-stateful-component' + ) + const pagePath = join('pages', 'hmr', 'style-stateful-component.js') + const originalContent = await next.readFile(pagePath) + try { + const pTag = await browser.elementByCss('.hmr-style-page p') + const initialFontSize = await pTag.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + await next.patchFile(pagePath, editedContent) + + // Check whether the this page has reloaded or not. + await retry(async () => { + const editedPTag = await browser.elementByCss('.hmr-style-page p') + expect(await editedPTag.getComputedCss('font-size')).toBe('200px') + }) + } finally { + await next.patchFile(pagePath, originalContent) + } + }) + + // Added because of a regression in react-hot-loader, see issues: #4246 #4273 + // Also: https://github.com/vercel/styled-jsx/issues/425 + it('should update styles in a dynamic component correctly', async () => { + const browser = await next.browser( + basePath + '/hmr/style-dynamic-component' + ) + const secondBrowser = await next.browser( + basePath + '/hmr/style-dynamic-component' + ) + const pagePath = join('components', 'hmr', 'dynamic.js') + const originalContent = await next.readFile(pagePath) + try { + const div = await browser.elementByCss('#dynamic-component') + const initialClientClassName = await div.getAttribute('class') + const initialFontSize = await div.getComputedCss('font-size') + + expect(initialFontSize).toBe('100px') + + const initialHtml = await renderViaHTTP( + next.url, + basePath + '/hmr/style-dynamic-component' + ) + expect(initialHtml.includes('100px')).toBeTruthy() + + const $initialHtml = cheerio.load(initialHtml) + const initialServerClassName = + $initialHtml('#dynamic-component').attr('class') + + expect(initialClientClassName === initialServerClassName).toBeTruthy() + + const editedContent = originalContent.replace('100px', '200px') + + // Change the page + await next.patchFile(pagePath, editedContent) + + // wait for 5 seconds + await waitFor(5000) + + // Check whether the this page has reloaded or not. + const editedDiv = + await secondBrowser.elementByCss('#dynamic-component') + const editedClientClassName = await editedDiv.getAttribute('class') + const editedFontSize = await editedDiv.getComputedCss('font-size') + const browserHtml = await secondBrowser.eval( + 'document.documentElement.innerHTML' + ) + + expect(editedFontSize).toBe('200px') + expect(browserHtml.includes('font-size:200px')).toBe(true) + expect(browserHtml.includes('font-size:100px')).toBe(false) + + const editedHtml = await renderViaHTTP( + next.url, + basePath + '/hmr/style-dynamic-component' + ) + expect(editedHtml.includes('200px')).toBeTruthy() + const $editedHtml = cheerio.load(editedHtml) + const editedServerClassName = + $editedHtml('#dynamic-component').attr('class') + + expect(editedClientClassName === editedServerClassName).toBe(true) + } finally { + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. + await next.patchFile(pagePath, originalContent) + } + }) + + it('should not full reload when nonlatin characters are used', async () => { + const browser = await next.browser(basePath + '/hmr/nonlatin') + const pagePath = join('pages', 'hmr', 'nonlatin.js') + const originalContent = await next.readFile(pagePath) + try { + const timeOrigin = await browser.eval('performance.timeOrigin') + const editedContent = originalContent.replace( + '
テスト
', + '
テスト
' + ) + + // Change the page + await next.patchFile(pagePath, editedContent) + + await browser.waitForElementByCss('.updated') + + expect(await browser.eval('performance.timeOrigin')).toEqual( + timeOrigin + ) + } finally { + // Finally is used so that we revert the content back to the original regardless of the test outcome + // restore the about page content. + await next.patchFile(pagePath, originalContent) + } + }) + }) + } +)