mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
### What? Upgrade jest to its latest version ### Why? #56899 fails because historically Jest stripped the globals in Node.js, but 28+ isn't doing that anymore. If we upgrade, we don't have to keep track of Node.js globals and when they are added. This will be useful in removing even more polyfills for things that are natively shipped in Node.js now. ### How? Jest 29 introduced a change to the snapshot format: https://jestjs.io/blog/2022/08/25/jest-29 First, I tried setting the old compat option to not require updating snapshots, but some tests were still failing: https://dev.azure.com/nextjs/next.js/_build/results?buildId=70633&view=logs&j=8af7cf9c-43a1-584d-6f5c-57bad8880974&t=7ae70e63-3625-50f4-6764-5b3e72b4bd7a&l=273 So going through the pain now instead.
1166 lines
37 KiB
TypeScript
1166 lines
37 KiB
TypeScript
import { join } from 'path'
|
||
import cheerio from 'cheerio'
|
||
import webdriver from 'next-webdriver'
|
||
import {
|
||
check,
|
||
getBrowserBodyText,
|
||
getRedboxHeader,
|
||
getRedboxSource,
|
||
hasRedbox,
|
||
renderViaHTTP,
|
||
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 () => {
|
||
let browser
|
||
const aboutPage = join('pages', 'hmr', 'about2.js')
|
||
const aboutContent = await next.readFile(aboutPage)
|
||
try {
|
||
browser = await webdriver(next.url, basePath + '/hmr/about2')
|
||
await check(
|
||
() => getBrowserBodyText(browser),
|
||
/This is the about page/
|
||
)
|
||
|
||
await next.patchFile(aboutPage, aboutContent.replace('</div>', 'div'))
|
||
|
||
expect(await hasRedbox(browser, true)).toBe(true)
|
||
expect(await getRedboxSource(browser)).toMatch(/Unexpected eof/)
|
||
|
||
await next.patchFile(aboutPage, aboutContent)
|
||
|
||
await check(
|
||
() => getBrowserBodyText(browser),
|
||
/This is the about page/
|
||
)
|
||
|
||
expect(next.cliOutput).toContain('Compiled /_error')
|
||
} 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 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, true)).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, true)).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, true)).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, true)).toBe(true)
|
||
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
|
||
" 1 of 1 unhandled error
|
||
Server Error
|
||
|
||
Error: The default export is not a React Component in page: "/hmr/about5"
|
||
|
||
This error happened while generating the page. Any console logs will be displayed in the terminal window."
|
||
`)
|
||
|
||
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, true)).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, true)).toBe(true)
|
||
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
|
||
" 1 of 1 unhandled error
|
||
Server Error
|
||
|
||
Error: The default export is not a React Component in page: "/hmr/about7"
|
||
|
||
This error happened while generating the page. Any console logs will be displayed in the terminal window."
|
||
`)
|
||
|
||
await next.patchFile(aboutPage, aboutContent)
|
||
|
||
await check(
|
||
() => getBrowserBodyText(browser),
|
||
/This is the about page/
|
||
)
|
||
expect(await hasRedbox(browser, false)).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, true)).toBe(true)
|
||
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(
|
||
`"Failed to compile"`
|
||
)
|
||
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, false)).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, true)).toBe(true)
|
||
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(
|
||
`"Failed to compile"`
|
||
)
|
||
let redboxSource = await getRedboxSource(browser)
|
||
|
||
redboxSource = redboxSource.replace(`${next.testDir}`, '.')
|
||
redboxSource = redboxSource.substring(
|
||
0,
|
||
redboxSource.indexOf('`----')
|
||
)
|
||
|
||
expect(
|
||
next.normalizeTestDirContent(redboxSource)
|
||
).toMatchInlineSnapshot(
|
||
next.normalizeSnapshot(`
|
||
"./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, false)).toBe(false)
|
||
} catch (err) {
|
||
await next.patchFile(aboutPage, aboutContent)
|
||
|
||
if (browser) {
|
||
await check(
|
||
() => getBrowserBodyText(browser),
|
||
/This is the about page/
|
||
)
|
||
}
|
||
|
||
if (!process.env.NEXT_SWC_DEV_BIN) {
|
||
throw err
|
||
}
|
||
} 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, true)).toBe(true)
|
||
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
|
||
" 1 of 1 unhandled error
|
||
Unhandled Runtime Error
|
||
|
||
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, true)).toBe(true)
|
||
expect(await getRedboxHeader(browser)).toMatchInlineSnapshot(`
|
||
" 1 of 1 unhandled error
|
||
Server Error
|
||
|
||
Error: an-expected-error-in-gip
|
||
|
||
This error happened while generating the page. Any console logs will be displayed in the terminal window."
|
||
`)
|
||
|
||
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()
|
||
})
|
||
})
|
||
|
||
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, true)).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[1])
|
||
if (timeUnit === 's') {
|
||
compileTimeMs = compileTimeMs * 1000
|
||
}
|
||
expect(compileTimeMs).toBeLessThan(3000)
|
||
} finally {
|
||
await next.patchFile(pageName, originalContent)
|
||
}
|
||
})
|
||
}
|
||
)
|