From c18fcf28fe7f4eb4689123e5d8884651106a8a98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Kud=C5=82a?= Date: Tue, 30 Jul 2024 03:36:43 +0200 Subject: [PATCH] Fix hmr assetPrefix escaping and reuse logic from other files (#67983) ### Fixing a bug fixes #63820 fixes #55320 fixes #52931 In one of the issues, there was a suggestion to delete `assetPrefix` from `next.config`. It helps if you have `assetPrefix: '/'`. Otherwise, you probably won't face this problem. **Explanation:** The problem lies where the browser tries to connect and where websocket is available on the server. Adjust the condition for `prefix` in `get-socket-url` as it can return `''` which will lead to `//` in the url. The browser wants to connect to `ws://localhost:3000/_next/webpack-hmr` While internally next exposes the web socket under `ws://localhost:3000//_next/webpack-hmr` - you can connect to it via wscat, postman or whatever. As the path is different it does not handle HMR requests in the browser. In addition to that - Reuse logic and extract separate files as a helper in shared/lib. | Before | After | | ------ | ------- | | ![before - latest canary branch](https://github.com/user-attachments/assets/c26c8b20-1352-49c6-a099-101394438ba0) before - latest canary branch - we can't establish a connection to HMR ws | ![after](https://github.com/user-attachments/assets/26500e68-bc4d-44ca-b418-f9bda6bc9aa6) same example with local changes - connected to ws | --------- Co-authored-by: Jiwon Choi Co-authored-by: JJ Kasper --- .../internal/helpers/get-socket-url.ts | 20 +- .../react-dev-overlay/pages/websocket.ts | 24 +- packages/next/src/server/lib/router-server.ts | 9 +- .../src/shared/lib/normalized-asset-prefix.ts | 16 + test/development/basic/hmr.test.ts | 1722 ++++++++--------- 5 files changed, 866 insertions(+), 925 deletions(-) create mode 100644 packages/next/src/shared/lib/normalized-asset-prefix.ts diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts index 69e3eb7af9c7c..b9a01f9bcc71e 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/get-socket-url.ts @@ -1,3 +1,5 @@ +import { normalizedAssetPrefix } from '../../../../../shared/lib/normalized-asset-prefix' + function getSocketProtocol(assetPrefix: string): string { let protocol = window.location.protocol @@ -9,18 +11,16 @@ function getSocketProtocol(assetPrefix: string): string { return protocol === 'http:' ? 'ws' : 'wss' } -export function getSocketUrl(assetPrefix: string): string { +export function getSocketUrl(assetPrefix: string | undefined): string { const { hostname, port } = window.location - const protocol = getSocketProtocol(assetPrefix) - const normalizedAssetPrefix = assetPrefix.replace(/^\/+/, '') - - let url = `${protocol}://${hostname}:${port}${ - normalizedAssetPrefix ? `/${normalizedAssetPrefix}` : '' - }` + const protocol = getSocketProtocol(assetPrefix || '') + const prefix = normalizedAssetPrefix(assetPrefix) - if (normalizedAssetPrefix.startsWith('http')) { - url = `${protocol}://${normalizedAssetPrefix.split('://', 2)[1]}` + // if original assetPrefix is a full URL with protocol + // we just update to use the correct `ws` protocol + if (assetPrefix?.replace(/^\/+/, '').includes('://')) { + return `${protocol}://${prefix}` } - return url + return `${protocol}://${hostname}:${port}${prefix}` } diff --git a/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts b/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts index 315c4dfc974ee..9fdea5b9ddef2 100644 --- a/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts +++ b/packages/next/src/client/components/react-dev-overlay/pages/websocket.ts @@ -1,4 +1,5 @@ import type { HMR_ACTION_TYPES } from '../../../../server/dev/hot-reloader-types' +import { getSocketUrl } from '../internal/helpers/get-socket-url' let source: WebSocket @@ -6,17 +7,6 @@ type ActionCallback = (action: HMR_ACTION_TYPES) => void const eventCallbacks: Array = [] -function getSocketProtocol(assetPrefix: string): string { - let protocol = location.protocol - - try { - // assetPrefix is a url - protocol = new URL(assetPrefix).protocol - } catch {} - - return protocol === 'http:' ? 'ws' : 'wss' -} - export function addMessageListener(callback: ActionCallback) { eventCallbacks.push(callback) } @@ -62,17 +52,7 @@ export function connectHMR(options: { path: string; assetPrefix: string }) { timer = setTimeout(init, reconnections > 5 ? 5000 : 1000) } - const { hostname, port } = location - const protocol = getSocketProtocol(options.assetPrefix || '') - const assetPrefix = options.assetPrefix.replace(/^\/+/, '') - - let url = `${protocol}://${hostname}:${port}${ - assetPrefix ? `/${assetPrefix}` : '' - }` - - if (assetPrefix.startsWith('http')) { - url = `${protocol}://${assetPrefix.split('://', 2)[1]}` - } + const url = getSocketUrl(options.assetPrefix) source = new window.WebSocket(`${url}${options.path}`) source.onopen = handleOnline diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts index d6f22b970aa5b..fc9dfd76a0678 100644 --- a/packages/next/src/server/lib/router-server.ts +++ b/packages/next/src/server/lib/router-server.ts @@ -45,6 +45,7 @@ import { HMR_ACTIONS_SENT_TO_BROWSER, type AppIsrManifestAction, } from '../dev/hot-reloader-types' +import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix' const debug = setupDebug('next:router-server:main') const isNextFont = (pathname: string | null) => @@ -662,8 +663,14 @@ export async function initialize(opts: { if (opts.dev && developmentBundler && req.url) { const { basePath, assetPrefix } = config + let hmrPrefix = basePath + + // assetPrefix overrides basePath for HMR path + if (assetPrefix) { + hmrPrefix = normalizedAssetPrefix(assetPrefix) + } const isHMRRequest = req.url.startsWith( - ensureLeadingSlash(`${assetPrefix || basePath}/_next/webpack-hmr`) + ensureLeadingSlash(`${hmrPrefix}/_next/webpack-hmr`) ) // only handle HMR requests if the basePath in the request diff --git a/packages/next/src/shared/lib/normalized-asset-prefix.ts b/packages/next/src/shared/lib/normalized-asset-prefix.ts new file mode 100644 index 0000000000000..b7da7771915a6 --- /dev/null +++ b/packages/next/src/shared/lib/normalized-asset-prefix.ts @@ -0,0 +1,16 @@ +export function normalizedAssetPrefix(assetPrefix: string | undefined): string { + const escapedAssetPrefix = assetPrefix?.replace(/^\/+/, '') || false + + // assetPrefix as a url + if (escapedAssetPrefix && escapedAssetPrefix.startsWith('://')) { + return escapedAssetPrefix.split('://', 2)[1] + } + + // assetPrefix is set to `undefined` or '/' + if (!escapedAssetPrefix) { + return '' + } + + // assetPrefix is a common path but escaped so let's add one leading slash + return `/${escapedAssetPrefix}` +} diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index 797f83052ab7c..1d22ae0720a75 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -16,467 +16,457 @@ import { import { createNext } from 'e2e-utils' import { NextInstance } from 'e2e-utils' import { outdent } from 'outdent' - -describe.each([['', '/docs']])( - 'basic HMR, basePath: %p', - (basePath: string) => { - let next: NextInstance - - beforeAll(async () => { - next = await createNext({ - files: join(__dirname, 'hmr'), - nextConfig: { - basePath, - }, - }) +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 { basePath } = nextConfig + let next: NextInstance + + beforeAll(async () => { + next = await createNext({ + files: join(__dirname, 'hmr'), + nextConfig, }) - afterAll(() => next.destroy()) + }) + afterAll(() => next.destroy()) - it('should show hydration error correctly', async () => { - const browser = await webdriver(next.url, basePath + '/hydration-error', { - pushErrorAsConsoleLog: true, - }) - await retry(async () => { - const logs = await browser.log() - expect(logs).toEqual( - expect.arrayContaining([ - { - message: expect.stringContaining( - 'https://react.dev/link/hydration-mismatch' - ), - source: 'error', - }, - ]) - ) - }) + it('should show hydration error correctly', async () => { + const browser = await webdriver(next.url, basePath + '/hydration-error', { + pushErrorAsConsoleLog: true, }) - - it('should have correct router.isReady for auto-export page', async () => { - let browser = await webdriver( - next.url, - basePath + '/auto-export-is-ready' + await retry(async () => { + const logs = await browser.log() + expect(logs).toEqual( + expect.arrayContaining([ + { + message: expect.stringContaining( + 'https://react.dev/link/hydration-mismatch' + ), + source: 'error', + }, + ]) ) + }) + }) - expect(await browser.elementByCss('#ready').text()).toBe('yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual( - {} - ) + it('should have correct router.isReady for auto-export page', async () => { + let browser = await webdriver(next.url, basePath + '/auto-export-is-ready') - browser = await webdriver( - next.url, - basePath + '/auto-export-is-ready?hello=world' - ) + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) - await check(async () => { - return browser.elementByCss('#ready').text() - }, 'yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ - hello: 'world', - }) + browser = await webdriver( + next.url, + basePath + '/auto-export-is-ready?hello=world' + ) + + await check(async () => { + return browser.elementByCss('#ready').text() + }, '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 webdriver(next.url, basePath + '/gsp-is-ready') + it('should have correct router.isReady for getStaticProps page', async () => { + let browser = await webdriver(next.url, basePath + '/gsp-is-ready') - expect(await browser.elementByCss('#ready').text()).toBe('yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual( - {} - ) + expect(await browser.elementByCss('#ready').text()).toBe('yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({}) - browser = await webdriver( - next.url, - basePath + '/gsp-is-ready?hello=world' - ) + browser = await webdriver(next.url, basePath + '/gsp-is-ready?hello=world') - await check(async () => { - return browser.elementByCss('#ready').text() - }, 'yes') - expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ - hello: 'world', - }) + await check(async () => { + return browser.elementByCss('#ready').text() + }, 'yes') + expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({ + hello: 'world', }) + }) - 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.') + 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) + // Rename the file to mimic a deleted page + await next.renameFile(contactPagePath, newContactPagePath) - await check( - () => getBrowserBodyText(browser), - /This page could not be found/ - ) + await check( + () => getBrowserBodyText(browser), + /This page could not be found/ + ) - // Rename the file back to the original filename - await next.renameFile(newContactPagePath, contactPagePath) + // Rename the file back to the original filename + await next.renameFile(newContactPagePath, contactPagePath) - // wait until the page comes back - await check( - () => getBrowserBodyText(browser), - /This is the contact page/ - ) + // wait until the page comes back + await check( + () => getBrowserBodyText(browser), + /This is the contact page/ + ) - expect(next.cliOutput).toContain('Compiled /_error') - } finally { - if (browser) { - await browser.close() - } - await next - .renameFile(newContactPagePath, contactPagePath) - .catch(() => {}) + 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') + 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 originalContent = await next.readFile(aboutPagePath) - const editedContent = originalContent.replace( - 'This is the about page', - 'COOL page' - ) + const aboutPagePath = join('pages', 'hmr', 'about.js') - // change the content - try { - await next.patchFile(aboutPagePath, editedContent) - await check(() => getBrowserBodyText(browser), /COOL page/) - } finally { - // add the original content - await next.patchFile(aboutPagePath, originalContent) - } + const originalContent = await next.readFile(aboutPagePath) + const editedContent = originalContent.replace( + 'This is the about page', + 'COOL page' + ) - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + // change the content + try { + await next.patchFile(aboutPagePath, editedContent) + await check(() => getBrowserBodyText(browser), /COOL page/) } finally { - if (browser) { - await browser.close() - } + // add the original content + await next.patchFile(aboutPagePath, originalContent) } - }) - 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 check(() => browser.elementByCss('p').text(), /COUNT: 2/) - } finally { - // restore the about page content. - await next.patchFile(aboutPagePath, originalContent) - } - } finally { - if (browser) { - await browser.close() - } + await check( + () => getBrowserBodyText(browser), + /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' + ) - // 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 check(async () => { - const editedPTag = - await browser.elementByCss('.hmr-style-page p') - return editedPTag.getComputedCss('font-size') - }, /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) - } + // Change the about.js page + await next.patchFile(aboutPagePath, editedContent) + + // Check whether the this page has reloaded or not. + await check(() => browser.elementByCss('p').text(), /COUNT: 2/) } finally { - if (browser) { - await browser.close() - } + // 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 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') + // 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 editedContent = originalContent.replace('100px', '200px') + expect(initialFontSize).toBe('100px') - // Change the page - await next.patchFile(pagePath, editedContent) + 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 check(async () => { const editedPTag = await browser.elementByCss('.hmr-style-page p') return editedPTag.getComputedCss('font-size') }, /200px/) } finally { - if (browser) { - await browser.close() - } + // 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 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') + } finally { + if (browser) { + await browser.close() + } + } + }) - expect(initialFontSize).toBe('100px') + // 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') - const initialHtml = await renderViaHTTP( - next.url, - basePath + '/hmr/style-dynamic-component' - ) - expect(initialHtml.includes('100px')).toBeTruthy() + expect(initialFontSize).toBe('100px') + const editedContent = originalContent.replace('100px', '200px') - const $initialHtml = cheerio.load(initialHtml) - const initialServerClassName = - $initialHtml('#dynamic-component').attr('class') + // Change the page + await next.patchFile(pagePath, editedContent) - expect( - initialClientClassName === initialServerClassName - ).toBeTruthy() + // Check whether the this page has reloaded or not. + await check(async () => { + const editedPTag = await browser.elementByCss('.hmr-style-page p') + return editedPTag.getComputedCss('font-size') + }, /200px/) + } finally { + if (browser) { + await browser.close() + } + await next.patchFile(pagePath, originalContent) + } + }) - const editedContent = originalContent.replace('100px', '200px') + // 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') - // Change the page - await next.patchFile(pagePath, editedContent) + expect(initialFontSize).toBe('100px') - // wait for 5 seconds - await waitFor(5000) + const initialHtml = await renderViaHTTP( + next.url, + basePath + '/hmr/style-dynamic-component' + ) + expect(initialHtml.includes('100px')).toBeTruthy() - 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' - ) + const $initialHtml = cheerio.load(initialHtml) + const initialServerClassName = + $initialHtml('#dynamic-component').attr('class') - expect(editedFontSize).toBe('200px') - expect(browserHtml.includes('font-size:200px')).toBe(true) - expect(browserHtml.includes('font-size:100px')).toBe(false) + expect(initialClientClassName === initialServerClassName).toBeTruthy() - 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') + const editedContent = originalContent.replace('100px', '200px') - 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) + // Change the page + await next.patchFile(pagePath, editedContent) - if (browser) { - await browser.close() - } + // wait for 5 seconds + await waitFor(5000) - if (secondBrowser) { - secondBrowser.close() - } - } - }) + 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' + ) - 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( - '
テスト
', - '
テスト
' - ) + expect(editedFontSize).toBe('200px') + expect(browserHtml.includes('font-size:200px')).toBe(true) + expect(browserHtml.includes('font-size:100px')).toBe(false) - // Change the page - await next.patchFile(pagePath, editedContent) + 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') - await browser.waitForElementByCss('.updated') + 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) - 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() + } - if (browser) { - await browser.close() - } + if (secondBrowser) { + secondBrowser.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') + 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/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
)' + browser = await webdriver(next.url, basePath + '/hmr/nonlatin') + const timeOrigin = await browser.eval('performance.timeOrigin') + const editedContent = originalContent.replace( + '
テスト
', + '
テスト
' ) - await check(() => getBrowserBodyText(browser), /the-new-page/) + // Change the page + await next.patchFile(pagePath, editedContent) - await next.deleteFile(newPage) + await browser.waitForElementByCss('.updated') - await check( - () => getBrowserBodyText(browser), - /This page could not be found/ + expect(await browser.eval('performance.timeOrigin')).toEqual( + timeOrigin ) - - expect(next.cliOutput).toContain('Compiled /_error') - } catch (err) { - await next.deleteFile(newPage) - throw err } 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() } } }) + }) + }) - 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') + 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/foo/page') + try { + browser = await webdriver(next.url, basePath + '/hmr/new-page') - expect(await browser.elementByCss('body').text()).toMatch( - /This page could not be found/ - ) + 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
)' - ) + // Add the page + await next.patchFile( + newPage, + 'export default () => (
the-new-page
)' + ) - await check(() => getBrowserBodyText(browser), /the-new-page/) + await check(() => getBrowserBodyText(browser), /the-new-page/) - await next.deleteFile(newPage) + await next.deleteFile(newPage) - await check( - () => getBrowserBodyText(browser), - /This page could not be found/ - ) + await check( + () => getBrowserBodyText(browser), + /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() - } + expect(next.cliOutput).toContain('Compiled /_error') + } catch (err) { + await next.deleteFile(newPage) + throw err + } finally { + if (browser) { + await browser.close() } - }) + } + }) - it('should not continously poll a custom error page', async () => { - const errorPage = join('pages', '_error.js') + 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( - errorPage, - outdent` + newPage, + 'export default () => (
the-new-page
)' + ) + + await check(() => getBrowserBodyText(browser), /the-new-page/) + + await next.deleteFile(newPage) + + await check( + () => getBrowserBodyText(browser), + /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 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 (
@@ -496,46 +486,46 @@ describe.each([['', '/docs']])( export default Error ` - ) + ) - try { - // navigate to a 404 page - await webdriver(next.url, basePath + '/does-not-exist') + try { + // navigate to a 404 page + await webdriver(next.url, basePath + '/does-not-exist') - await check(() => next.cliOutput, /getInitialProps called/) + await check(() => next.cliOutput, /getInitialProps called/) - const outputIndex = next.cliOutput.length + const outputIndex = next.cliOutput.length - // wait a few seconds to ensure polling didn't happen - await waitFor(3000) + // 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) - } finally { - await next.deleteFile(errorPage) - } - }) + const logOccurrences = + next.cliOutput.slice(outputIndex).split('getInitialProps called') + .length - 1 + 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/ - ) - }) + 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 next.patchFile(aboutPage, aboutContent.replace('', 'div')) - await assertHasRedbox(browser) - const source = next.normalizeTestDirContent( - await getRedboxSource(browser) - ) - if (basePath === '' && !process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + 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] @@ -558,8 +548,8 @@ describe.each([['', '/docs']])( Import trace for requested module: ./pages/hmr/about2.js" `) - } else if (basePath === '' && process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + } else if (basePath === '' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js:7:1 Parsing ecmascript source code failed 5 | div @@ -570,8 +560,8 @@ describe.each([['', '/docs']])( Unexpected token. Did you mean \`{'}'}\` or \`}\`?" `) - } else if (basePath === '/docs' && !process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + } else if (basePath === '/docs' && !process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js Error: x Unexpected token. Did you mean \`{'}'}\` or \`}\`? ,-[7:1] @@ -594,8 +584,8 @@ describe.each([['', '/docs']])( Import trace for requested module: ./pages/hmr/about2.js" `) - } else if (basePath === '/docs' && process.env.TURBOPACK) { - expect(source).toMatchInlineSnapshot(` + } else if (basePath === '/docs' && process.env.TURBOPACK) { + expect(source).toMatchInlineSnapshot(` "./pages/hmr/about2.js:7:1 Parsing ecmascript source code failed 5 | div @@ -606,321 +596,285 @@ describe.each([['', '/docs']])( Unexpected token. Did you mean \`{'}'}\` or \`}\`?" `) - } + } - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) - await retry(async () => { - expect(await getBrowserBodyText(browser)).toMatch( - /This is the about page/ - ) - }) + 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') + 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') - ) + await next.patchFile(aboutPage, aboutContent.replace('', 'div')) - // Ensure dev server has time to break: - await new Promise((resolve) => setTimeout(resolve, 2000)) + // Ensure dev server has time to break: + await new Promise((resolve) => setTimeout(resolve, 2000)) - browser = await webdriver(next.url, basePath + '/hmr/contact') + browser = await webdriver(next.url, basePath + '/hmr/contact') - await assertHasRedbox(browser) - expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) + await assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + await check( + () => getBrowserBodyText(browser), + /This is the contact page/ + ) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the contact page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the contact page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + 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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + 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 check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace('export', 'aa=20;\nexport') - ) + await next.patchFile( + aboutPage, + aboutContent.replace('export', 'aa=20;\nexport') + ) - await assertHasRedbox(browser) - expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } finally { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await browser.close() - } + await check(() => getBrowserBodyText(browser), /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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + 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 check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'return', - 'throw new Error("an-expected-error");\nreturn' - ) + 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 assertHasRedbox(browser) + expect(await getRedboxSource(browser)).toMatch(/an-expected-error/) - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + await check(() => getBrowserBodyText(browser), /This is the about page/) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + 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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + 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 check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default {};\nexport const fn =' - ) + 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 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 next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + 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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + 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 check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default () => /search/;\nexport const fn =' - ) + 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 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 next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + 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') + 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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.url, basePath + '/hmr/about7') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'export default undefined;\nexport const fn =' - ) + 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 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 next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - await assertNoRedbox(browser) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + 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') + 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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.appPort, basePath + '/hmr/about8') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'import "../../components/parse-error.xyz"\nexport default' - ) + 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') + await assertHasRedbox(browser) + expect(await getRedboxHeader(browser)).toMatch('Failed to compile') - if (process.env.TURBOPACK) { - expect(await getRedboxSource(browser)).toMatchInlineSnapshot(` + 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(` + } 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 @@ -934,60 +888,54 @@ describe.each([['', '/docs']])( ./components/parse-error.xyz ./pages/hmr/about8.js" `) - } - await next.patchFile(aboutPage, aboutContent) + } + await next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - await assertNoRedbox(browser) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } + } - throw err - } finally { - if (browser) { - await browser.close() - } + 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') + 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 check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) + const aboutContent = await next.readFile(aboutPage) + try { + browser = await webdriver(next.appPort, basePath + '/hmr/about9') + await check(() => getBrowserBodyText(browser), /This is the about page/) - await next.patchFile( - aboutPage, - aboutContent.replace( - 'export default', - 'import "../../components/parse-error.js"\nexport default' - ) + 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) + 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(` + 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 @@ -999,14 +947,14 @@ describe.each([['', '/docs']])( Expression expected" `) - } else { - redboxSource = redboxSource.substring( - 0, - redboxSource.indexOf('`----') - ) + } else { + redboxSource = redboxSource.substring( + 0, + redboxSource.indexOf('`----') + ) - expect(next.normalizeTestDirContent(redboxSource)) - .toMatchInlineSnapshot(` + expect(next.normalizeTestDirContent(redboxSource)) + .toMatchInlineSnapshot(` "./components/parse-error.js Error: x Expression expected ,-[3:1] @@ -1018,249 +966,239 @@ describe.each([['', '/docs']])( 5 | js " `) - } + } - await next.patchFile(aboutPage, aboutContent) + await next.patchFile(aboutPage, aboutContent) + + await check(() => getBrowserBodyText(browser), /This is the about page/) + await assertNoRedbox(browser) + } catch (err) { + await next.patchFile(aboutPage, aboutContent) + if (browser) { await check( () => getBrowserBodyText(browser), /This is the about page/ ) - await assertNoRedbox(browser) - } catch (err) { - await next.patchFile(aboutPage, aboutContent) - - if (browser) { - await check( - () => getBrowserBodyText(browser), - /This is the about page/ - ) - } - } finally { - if (browser) { - await browser.close() - } } - }) + } 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() + 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 assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) - await next.patchFile( - erroredPage, - errorContent.replace('throw error', 'return {}') - ) + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) - await check(() => getBrowserBodyText(browser), /Hello/) + await check(() => getBrowserBodyText(browser), /Hello/) - await next.patchFile(erroredPage, errorContent) + await next.patchFile(erroredPage, errorContent) - await check(async () => { - await browser.refresh() - await waitFor(2000) - const text = await getBrowserBodyText(browser) - if (text.includes('Hello')) { - throw new Error('waiting') - } - return getRedboxSource(browser) - }, /an-expected-error-in-gip/) - } catch (err) { - await next.patchFile(erroredPage, errorContent) - - throw err - } finally { - if (browser) { - await browser.close() + await check(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') } - } - }) + return getRedboxSource(browser) + }, /an-expected-error-in-gip/) + } catch (err) { + await next.patchFile(erroredPage, errorContent) - 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') + throw err + } finally { + if (browser) { + await browser.close() + } + } + }) - await assertHasRedbox(browser) - expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( - `"Error: an-expected-error-in-gip"` - ) + 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') - const erroredPage = join('pages', 'hmr', 'error-in-gip.js') + await assertHasRedbox(browser) + expect(await getRedboxDescription(browser)).toMatchInlineSnapshot( + `"Error: an-expected-error-in-gip"` + ) - await next.patchFile( - erroredPage, - errorContent.replace('throw error', 'return {}') - ) + const erroredPage = join('pages', 'hmr', 'error-in-gip.js') - await check(() => getBrowserBodyText(browser), /Hello/) + await next.patchFile( + erroredPage, + errorContent.replace('throw error', 'return {}') + ) - await next.patchFile(erroredPage, errorContent) + await check(() => getBrowserBodyText(browser), /Hello/) - await check(async () => { - await browser.refresh() - await waitFor(2000) - const text = await getBrowserBodyText(browser) - if (text.includes('Hello')) { - throw new Error('waiting') - } - return getRedboxSource(browser) - }, /an-expected-error-in-gip/) - } catch (err) { - await next.patchFile(erroredPage, errorContent) + await next.patchFile(erroredPage, errorContent) - throw err - } finally { - if (browser) { - await browser.close() + await check(async () => { + await browser.refresh() + await waitFor(2000) + const text = await getBrowserBodyText(browser) + if (text.includes('Hello')) { + throw new Error('waiting') } + return getRedboxSource(browser) + }, /an-expected-error-in-gip/) + } catch (err) { + await next.patchFile(erroredPage, errorContent) + + throw err + } finally { + if (browser) { + await browser.close() } - }) + } }) + }) - describe('Full reload', () => { - it('should warn about full reload in cli output - anonymous page function', async () => { - const start = next.cliOutput.length - const browser = await webdriver( - next.url, - 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' + describe('Full reload', () => { + it('should warn about full reload in cli output - anonymous page function', async () => { + const start = next.cliOutput.length + const browser = await webdriver( + next.url, + 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) + 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 - ) + 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!!!' - ) + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'hello world!!!' + ) - // CLI warning - expect(next.cliOutput.slice(start)).toContain(cliWarning) + // 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." - ) + // 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 webdriver( - next.url, - basePath + '/hmr/runtime-error' - ) - const cliWarning = - 'Fast Refresh had to perform a full reload due to a runtime error.' - - await check( - () => getRedboxHeader(browser), - /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) + ).toBeTruthy() + }) - expect(await browser.waitForElementByCss('#updated').text()).toBe( - 'whoops' - ) + it('should warn about full reload in cli output - runtime-error', async () => { + const start = next.cliOutput.length + const browser = await webdriver(next.url, basePath + '/hmr/runtime-error') + const cliWarning = + 'Fast Refresh had to perform a full reload due to a runtime error.' - // CLI warning - expect(next.cliOutput.slice(start)).toContain(cliWarning) + await check( + () => getRedboxHeader(browser), + /ReferenceError: whoops is not defined/ + ) + expect(next.cliOutput.slice(start)).not.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() - }) - }) + 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) - 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') - }) - } + expect(await browser.waitForElementByCss('#updated').text()).toBe( + 'whoops' + ) - it('should have correct compile timing after fixing error', async () => { - const pageName = 'pages/auto-export-is-ready.js' - const originalContent = await next.readFile(pageName) + // CLI warning + expect(next.cliOutput.slice(start)).toContain(cliWarning) - try { - const browser = await webdriver( - next.url, - 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 check( - () => next.cliOutput.substring(outputLength), - /Compiled.*?/i + // 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' + ) ) - const compileTimeStr = next.cliOutput.substring(outputLength) + ).toBeTruthy() + }) + }) + + 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') + }) + } - const matches = [ - ...compileTimeStr.match(/Compiled.*? in ([\d.]{1,})\s?(?:s|ms)/i), - ] - const [, compileTime, timeUnit] = matches + it('should have correct compile timing after fixing error', async () => { + const pageName = 'pages/auto-export-is-ready.js' + const originalContent = await next.readFile(pageName) - let compileTimeMs = parseFloat(compileTime) - if (timeUnit === 's') { - compileTimeMs = compileTimeMs * 1000 - } - expect(compileTimeMs).toBeLessThan(3000) - } finally { - await next.patchFile(pageName, originalContent) + try { + const browser = await webdriver( + next.url, + 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 check(() => next.cliOutput.substring(outputLength), /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) + } + }) +})