aeafed9405
This causes Turbopack to fail and communicate when a file with an unhandled or unregistered extension is built. Test Plan: `TURBOPACK=1 pnpm test-dev test/development/basic/hmr.test.ts` Closes PACK-2803
1234 lines
39 KiB
TypeScript
1234 lines
39 KiB
TypeScript
import { join } from 'path'
|
|
import cheerio from 'cheerio'
|
|
import webdriver from 'next-webdriver'
|
|
import {
|
|
check,
|
|
getBrowserBodyText,
|
|
getRedboxHeader,
|
|
getRedboxDescription,
|
|
getRedboxSource,
|
|
hasRedbox,
|
|
renderViaHTTP,
|
|
retry,
|
|
waitFor,
|
|
} from 'next-test-utils'
|
|
import { createNext } from 'e2e-utils'
|
|
import { NextInstance } from 'test/lib/next-modes/base'
|
|
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,
|
|
},
|
|
})
|
|
})
|
|
afterAll(() => next.destroy())
|
|
|
|
it('should show hydration error correctly', async () => {
|
|
const browser = await webdriver(next.url, basePath + '/hydration-error')
|
|
await check(async () => {
|
|
const logs = await browser.log()
|
|
return logs.some((log) =>
|
|
log.message.includes('messages/react-hydration-error')
|
|
)
|
|
? 'success'
|
|
: JSON.stringify(logs, null, 2)
|
|
}, 'success')
|
|
})
|
|
|
|
it('should have correct router.isReady for auto-export page', async () => {
|
|
let browser = await webdriver(
|
|
next.url,
|
|
basePath + '/auto-export-is-ready'
|
|
)
|
|
|
|
expect(await browser.elementByCss('#ready').text()).toBe('yes')
|
|
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual(
|
|
{}
|
|
)
|
|
|
|
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')
|
|
|
|
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'
|
|
)
|
|
|
|
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.')
|
|
|
|
// Rename the file to mimic a deleted page
|
|
await next.renameFile(contactPagePath, newContactPagePath)
|
|
|
|
await check(
|
|
() => getBrowserBodyText(browser),
|
|
/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 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(() => {})
|
|
}
|
|
})
|
|
})
|
|
|
|
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 check(() => getBrowserBodyText(browser), /COOL page/)
|
|
} finally {
|
|
// add the original content
|
|
await next.patchFile(aboutPagePath, originalContent)
|
|
}
|
|
|
|
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'
|
|
)
|
|
|
|
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()
|
|
}
|
|
}
|
|
})
|
|
|
|
// 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)
|
|
}
|
|
} 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 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)
|
|
}
|
|
})
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
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 () => (<div id="new-page">the-new-page</div>)'
|
|
)
|
|
|
|
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 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 () => (<div id="new-page">the-new-page</div>)'
|
|
)
|
|
|
|
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 (
|
|
<div>
|
|
Error Message: {message}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
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 check(() => next.cliOutput, /getInitialProps called/)
|
|
|
|
const outputIndex = next.cliOutput.length
|
|
|
|
// wait a few seconds to ensure polling didn't happen
|
|
await waitFor(3000)
|
|
|
|
const logOccurrences =
|
|
next.cliOutput.slice(outputIndex).split('getInitialProps called')
|
|
.length - 1
|
|
expect(logOccurrences).toBe(0)
|
|
} 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>', 'div'))
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
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 \`}\`?
|
|
,-[TEST_DIR/pages/hmr/about2.js:4:1]
|
|
4 | <p>This is the about page.</p>
|
|
5 | div
|
|
6 | )
|
|
7 | }
|
|
: ^
|
|
\`----
|
|
|
|
x Unexpected eof
|
|
,-[TEST_DIR/pages/hmr/about2.js:5:1]
|
|
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 \`}\`?
|
|
,-[TEST_DIR/pages/hmr/about2.js:4:1]
|
|
4 | <p>This is the about page.</p>
|
|
5 | div
|
|
6 | )
|
|
7 | }
|
|
: ^
|
|
\`----
|
|
|
|
x Unexpected eof
|
|
,-[TEST_DIR/pages/hmr/about2.js:5:1]
|
|
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>', 'div')
|
|
)
|
|
|
|
// Ensure dev server has time to break:
|
|
await new Promise((resolve) => setTimeout(resolve, 2000))
|
|
|
|
browser = await webdriver(next.url, basePath + '/hmr/contact')
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/)
|
|
|
|
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/
|
|
)
|
|
}
|
|
|
|
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/
|
|
)
|
|
|
|
await next.patchFile(
|
|
aboutPage,
|
|
aboutContent.replace('export', 'aa=20;\nexport')
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/)
|
|
|
|
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()
|
|
}
|
|
}
|
|
})
|
|
|
|
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'
|
|
)
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
expect(await getRedboxSource(browser)).toMatch(/an-expected-error/)
|
|
|
|
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/
|
|
)
|
|
}
|
|
|
|
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/
|
|
)
|
|
|
|
await next.patchFile(
|
|
aboutPage,
|
|
aboutContent.replace(
|
|
'export default',
|
|
'export default {};\nexport const fn ='
|
|
)
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
|
|
`"Error: The default export is not a React Component in page: "/hmr/about5""`
|
|
)
|
|
|
|
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/
|
|
)
|
|
}
|
|
|
|
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/
|
|
)
|
|
|
|
await next.patchFile(
|
|
aboutPage,
|
|
aboutContent.replace(
|
|
'export default',
|
|
'export default () => /search/;\nexport const fn ='
|
|
)
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
// 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 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()
|
|
}
|
|
}
|
|
})
|
|
|
|
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/
|
|
)
|
|
|
|
await next.patchFile(
|
|
aboutPage,
|
|
aboutContent.replace(
|
|
'export default',
|
|
'export default undefined;\nexport const fn ='
|
|
)
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
|
|
`"Error: The default export is not a React Component in page: "/hmr/about7""`
|
|
)
|
|
|
|
await next.patchFile(aboutPage, aboutContent)
|
|
|
|
await check(
|
|
() => getBrowserBodyText(browser),
|
|
/This is the about page/
|
|
)
|
|
expect(await hasRedbox(browser)).toBe(false)
|
|
} 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()
|
|
}
|
|
}
|
|
})
|
|
|
|
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/
|
|
)
|
|
|
|
await next.patchFile(
|
|
aboutPage,
|
|
aboutContent.replace(
|
|
'export default',
|
|
'import "../../components/parse-error.xyz"\nexport default'
|
|
)
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
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 check(
|
|
() => getBrowserBodyText(browser),
|
|
/This is the about page/
|
|
)
|
|
expect(await hasRedbox(browser)).toBe(false)
|
|
} 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()
|
|
}
|
|
}
|
|
})
|
|
|
|
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/
|
|
)
|
|
|
|
await next.patchFile(
|
|
aboutPage,
|
|
aboutContent.replace(
|
|
'export default',
|
|
'import "../../components/parse-error.js"\nexport default'
|
|
)
|
|
)
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
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
|
|
,-[./components/parse-error.js:1:1]
|
|
1 | This
|
|
2 | is
|
|
3 | }}}
|
|
: ^
|
|
4 | invalid
|
|
5 | js
|
|
"
|
|
`)
|
|
}
|
|
|
|
await next.patchFile(aboutPage, aboutContent)
|
|
|
|
await check(
|
|
() => getBrowserBodyText(browser),
|
|
/This is the about page/
|
|
)
|
|
expect(await hasRedbox(browser)).toBe(false)
|
|
} catch (err) {
|
|
await next.patchFile(aboutPage, aboutContent)
|
|
|
|
if (browser) {
|
|
await check(
|
|
() => getBrowserBodyText(browser),
|
|
/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()
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
|
|
`"Error: an-expected-error-in-gip"`
|
|
)
|
|
|
|
await next.patchFile(
|
|
erroredPage,
|
|
errorContent.replace('throw error', 'return {}')
|
|
)
|
|
|
|
await check(() => getBrowserBodyText(browser), /Hello/)
|
|
|
|
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()
|
|
}
|
|
}
|
|
})
|
|
|
|
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')
|
|
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
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 check(() => getBrowserBodyText(browser), /Hello/)
|
|
|
|
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()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
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)
|
|
|
|
const currentFileContent = await next.readFile(
|
|
'./pages/hmr/anonymous-page-function.js'
|
|
)
|
|
const newFileContent = currentFileContent.replace(
|
|
'<p>hello world</p>',
|
|
'<p id="updated">hello world!!!</p>'
|
|
)
|
|
await next.patchFile(
|
|
'./pages/hmr/anonymous-page-function.js',
|
|
newFileContent
|
|
)
|
|
|
|
expect(await browser.waitForElementByCss('#updated').text()).toBe(
|
|
'hello world!!!'
|
|
)
|
|
|
|
// CLI warning
|
|
expect(next.cliOutput.slice(start)).toContain(cliWarning)
|
|
|
|
// Browser warning
|
|
const browserLogs = await browser.log()
|
|
expect(
|
|
browserLogs.some(({ message }) =>
|
|
message.includes(
|
|
"Fast Refresh will perform a full reload when you edit a file that's imported by modules outside of the React rendering tree."
|
|
)
|
|
)
|
|
).toBeTruthy()
|
|
})
|
|
|
|
it('should warn about full reload in cli output - runtime-error', async () => {
|
|
const start = next.cliOutput.length
|
|
const browser = await 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',
|
|
'<p id="updated">whoops</p>'
|
|
)
|
|
await next.patchFile('./pages/hmr/runtime-error.js', newFileContent)
|
|
|
|
expect(await browser.waitForElementByCss('#updated').text()).toBe(
|
|
'whoops'
|
|
)
|
|
|
|
// CLI warning
|
|
expect(next.cliOutput.slice(start)).toContain(cliWarning)
|
|
|
|
// Browser warning
|
|
const browserLogs = await browser.log()
|
|
expect(
|
|
browserLogs.some(({ message }) =>
|
|
message.includes(
|
|
'[Fast Refresh] performing full reload because your application had an unrecoverable error'
|
|
)
|
|
)
|
|
).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
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)
|
|
|
|
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
|
|
)
|
|
expect(await hasRedbox(browser)).toBe(true)
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
)
|