first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runBasicHmrTest } from './run-basic-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - basic, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runBasicHmrTest(nextConfig)
})

View File

@@ -0,0 +1,12 @@
export default () => {
return (
<div id="dynamic-component">
Dynamic Component
<style jsx>{`
div {
font-size: 100px;
}
`}</style>
</div>
)
}

View File

@@ -0,0 +1,5 @@
This
is
}}}
invalid
js

View File

@@ -0,0 +1,5 @@
This
is
}}}
invalid
js

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runErrorRecoveryHmrTest } from './run-error-recovery-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - Error Recovery, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runErrorRecoveryHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runFullReloadHmrTest } from './run-full-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - Full Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runFullReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '', assetPrefix: '/asset-prefix' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,7 @@
import { runHotModuleReloadHmrTest } from './run-hot-module-reload-hmr-test.util'
const nextConfig = { basePath: '/docs', assetPrefix: '/asset-prefix' }
describe(`HMR - Hot Module Reload, nextConfig: ${JSON.stringify(nextConfig)}`, () => {
runHotModuleReloadHmrTest(nextConfig)
})

View File

@@ -0,0 +1,12 @@
import { useRouter } from 'next/router'
export default function Page(props) {
const router = useRouter()
return (
<>
<p>auto-export router.isReady</p>
<p id="query">{JSON.stringify(router.query)}</p>
<p id="ready">{router.isReady ? 'yes' : 'no'}</p>
</>
)
}

View File

@@ -0,0 +1,20 @@
import { useRouter } from 'next/router'
export default function Page(props) {
const router = useRouter()
return (
<>
<p>getStaticProps router.isReady</p>
<p id="query">{JSON.stringify(router.query)}</p>
<p id="ready">{router.isReady ? 'yes' : 'no'}</p>
</>
)
}
export function getStaticProps() {
return {
props: {
now: Date.now(),
},
}
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function About4() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-about-page">
<p>This is the about page.</p>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function () {
return <p>hello world</p>
}

View File

@@ -0,0 +1,7 @@
export default function Page() {
return (
<div className="hmr-contact-page">
<p>This is the contact page.</p>
</div>
)
}

View File

@@ -0,0 +1,19 @@
import React from 'react'
export default class Counter extends React.Component {
state = { count: 0 }
incr() {
const { count } = this.state
this.setState({ count: count + 1 })
}
render() {
return (
<div>
<p>COUNT: {this.state.count}</p>
<button onClick={() => this.incr()}>Increment</button>
</div>
)
}
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
export default class Page extends React.Component {
static getInitialProps() {
const error = new Error('an-expected-error-in-gip')
throw error
}
render() {
return <div>Hello</div>
}
}

View File

@@ -0,0 +1,11 @@
import Link from 'next/link'
export default function Page() {
return (
<div>
<Link href="/hmr/error-in-gip" id="error-in-gip-link">
Bad Page
</Link>
</div>
)
}

View File

@@ -0,0 +1,3 @@
export default function NonLatin(props) {
return <div>テスト</div>
}

View File

@@ -0,0 +1,4 @@
export default function () {
// eslint-disable-next-line no-undef
return whoops
}

View File

@@ -0,0 +1,8 @@
import React from 'react'
import dynamic from 'next/dynamic'
const HmrDynamic = dynamic(import('../../components/hmr/dynamic'))
export default function Page() {
return <HmrDynamic />
}

View File

@@ -0,0 +1,20 @@
import React, { Component } from 'react'
export default class StyleStateFul extends Component {
render() {
return (
<React.Fragment>
<div className="hmr-style-page">
<p>
This is the style page.
<style jsx>{`
p {
font-size: 100px;
}
`}</style>
</p>
</div>
</React.Fragment>
)
}
}

View File

@@ -0,0 +1,17 @@
import React from 'react'
export default function Style() {
return (
<React.Fragment>
<div className="hmr-style-page">
<p>
This is the style page.
<style jsx>{`
p {
font-size: 100px;
}
`}</style>
</p>
</div>
</React.Fragment>
)
}

View File

@@ -0,0 +1,3 @@
export default function Page(props) {
return <p>is server {typeof window}</p>
}

View File

@@ -0,0 +1,112 @@
import {
waitForRedbox,
getBrowserBodyText,
retry,
waitFor,
} from 'next-test-utils'
import { createNext, nextTestSetup } from 'e2e-utils'
export function runBasicHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next, isTurbopack } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
it('should have correct router.isReady for auto-export page', async () => {
let browser = await next.browser(basePath + '/auto-export-is-ready')
expect(await browser.elementByCss('#ready').text()).toBe('yes')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({})
browser = await next.browser(basePath + '/auto-export-is-ready?hello=world')
await retry(async () => {
expect(await browser.elementByCss('#ready').text()).toBe('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 next.browser(basePath + '/gsp-is-ready')
expect(await browser.elementByCss('#ready').text()).toBe('yes')
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({})
browser = await next.browser(basePath + '/gsp-is-ready?hello=world')
await retry(async () => {
expect(await browser.elementByCss('#ready').text()).toBe('yes')
})
expect(JSON.parse(await browser.elementByCss('#query').text())).toEqual({
hello: 'world',
})
})
;(isTurbopack ? it : it.skip)(
'should have correct compile timing after fixing error',
async () => {
const browser = await next.browser(basePath + '/auto-export-is-ready')
let outputLength
await next.patchFile(
'pages/auto-export-is-ready.js',
(content) => `import hello from 'non-existent'\n` + content,
async () => {
await waitForRedbox(browser)
await waitFor(3000)
outputLength = next.cliOutput.length
}
)
let compileTimeStr
await retry(async () => {
compileTimeStr = next.cliOutput.substring(outputLength)
expect(compileTimeStr).toMatch(/Compiled.*?/i)
})
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)
}
)
it('should reload the page when the server restarts', async () => {
const browser = await next.browser(basePath + '/hmr/about')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.destroy()
let reloadPromise = new Promise((resolve) => {
browser.on('request', (req) => {
if (req.url().endsWith('/hmr/about')) {
resolve(req.url())
}
})
})
const secondNext = await createNext({
files: __dirname,
nextConfig,
forcedPort: next.appPort,
})
await reloadPromise
await secondNext.destroy()
})
}

View File

@@ -0,0 +1,664 @@
import { join } from 'path'
import {
waitForRedbox,
waitForNoRedbox,
getBrowserBodyText,
getRedboxHeader,
getRedboxDescription,
getRedboxSource,
retry,
waitFor,
trimEndMultiline,
getDistDir,
} from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
import { outdent } from 'outdent'
export function runErrorRecoveryHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
it('should recover from 404 after a page has been added', async () => {
const browser = await next.browser(basePath + '/hmr/new-page')
expect(await browser.elementByCss('body').text()).toMatch(
/This page could not be found/
)
expect(next.cliOutput).toContain('GET /hmr/new-page 404')
let cliOutputLength = next.cliOutput.length
// Add the page
await next.patchFile(
join('pages', 'hmr', 'new-page.js'),
'export default () => (<div id="new-page">the-new-page</div>)',
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/new-page 200'
)
cliOutputLength = next.cliOutput.length
}
)
// page was deleted at the end of patchFile
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This page could not be found/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/new-page 404'
)
})
it('should recover from 404 after a page has been added with dynamic segments', async () => {
const browser = await next.browser(basePath + '/hmr/foo/page')
expect(await browser.elementByCss('body').text()).toMatch(
/This page could not be found/
)
expect(next.cliOutput).toContain('GET /hmr/foo/page 404')
let cliOutputLength = next.cliOutput.length
// Add the page
await next.patchFile(
join('pages', 'hmr', '[foo]', 'page.js'),
'export default () => (<div id="new-page">the-new-page</div>)',
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/the-new-page/)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/foo/page 200'
)
cliOutputLength = next.cliOutput.length
}
)
// page was deleted at the end of patchFile
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This page could not be found/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toContain(
'GET /hmr/foo/page 404'
)
})
;(process.env.IS_TURBOPACK_TEST ? it.skip : it)(
// this test fails frequently with turbopack
'should not continously poll a custom error page',
async () => {
await next.patchFile(
join('pages', '_error.js'),
outdent`
function Error({ statusCode, message, count }) {
return (
<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
`,
async () => {
// navigate to a 404 page
await next.browser(basePath + '/does-not-exist')
await retry(() => {
expect(next.cliOutput).toMatch(/getInitialProps called/)
})
const outputIndex = next.cliOutput.length
// wait a few seconds to ensure polling didn't happen
await waitFor(3000)
const logOccurrences =
next.cliOutput.slice(outputIndex).split('getInitialProps called')
.length - 1
expect(logOccurrences).toBe(0)
}
)
}
)
it('should detect syntax errors and recover', async () => {
const browser = await next.browser(basePath + '/hmr/about2')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about2.js'),
(content) => content.replace('</div>', 'div'),
async () => {
await waitForRedbox(browser)
const source = next.normalizeTestDirContent(
await getRedboxSource(browser)
)
if (process.env.IS_TURBOPACK_TEST) {
expect(source).toMatchInlineSnapshot(`
"./pages/hmr/about2.js (7:1)
Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
5 | div
6 | )
> 7 | }
| ^
8 |
Parsing ecmascript source code failed"
`)
} else if (process.env.NEXT_RSPACK) {
expect(trimEndMultiline(source)).toMatchInlineSnapshot(`
"./pages/hmr/about2.js
╰─▶ × Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
│ ,-[7:1]
│ 4 | <p>This is the about page.</p>
│ 5 | div
│ 6 | )
│ 7 | }
│ : ^
\`----
│ x Expected '</', got '<eof>'
│ ,-[7:3]
│ 5 | div
│ 6 | )
│ 7 | }
\`----
│ Caused by:
│ Syntax Error
Import trace for requested module:
./pages/hmr/about2.js"
`)
} else {
expect(source).toMatchInlineSnapshot(`
"./pages/hmr/about2.js
Error: x Unexpected token. Did you mean \`{'}'}\` or \`&rbrace;\`?
,-[7:1]
4 | <p>This is the about page.</p>
5 | div
6 | )
7 | }
: ^
\`----
x Expected '</', got '<eof>'
,-[7:3]
5 | div
6 | )
7 | }
\`----
Caused by:
Syntax Error
Import trace for requested module:
./pages/hmr/about2.js"
`)
}
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
if (!process.env.IS_TURBOPACK_TEST) {
// Turbopack doesn't have this restriction
it('should show the error on all pages', async () => {
const browser = await next.browser(basePath + '/hmr/contact')
await next.render(basePath + '/hmr/about2')
await next.patchFile(
join('pages', 'hmr', 'about2.js'),
(content) => content.replace('</div>', 'div'),
async () => {
// Ensure dev server has time to break:
await new Promise((resolve) => setTimeout(resolve, 2000))
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toContain(
"Expected '</', got '<eof>'"
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the contact page/
)
})
})
}
it('should detect runtime errors on the module scope', async () => {
const browser = await next.browser(basePath + '/hmr/about3')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about3.js'),
(content) => content.replace('export', 'aa=20;\nexport'),
async () => {
await waitForRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch(/aa is not defined/)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover from errors in the render function', async () => {
const browser = await next.browser(basePath + '/hmr/about4')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about4.js'),
(content) =>
content.replace(
'return',
'throw new Error("an-expected-error");\nreturn'
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxSource(browser)).toMatch(/an-expected-error/)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover after exporting an invalid page', async () => {
const browser = await next.browser(basePath + '/hmr/about5')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about5.js'),
(content) =>
content.replace(
'export default',
'export default {};\nexport const fn ='
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"The default export is not a React Component in page: "/hmr/about5""`
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover after a bad return from the render function', async () => {
const browser = await next.browser(basePath + '/hmr/about6')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about6.js'),
(content) =>
content.replace(
'export default',
'export default () => /search/;\nexport const fn ='
),
async () => {
await waitForRedbox(browser)
// TODO: Replace this when webpack 5 is the default
expect(await getRedboxHeader(browser)).toMatch(
`Objects are not valid as a React child (found: [object RegExp]). If you meant to render a collection of children, use an array instead.`
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should recover after undefined exported as default', async () => {
const browser = await next.browser(basePath + '/hmr/about7')
const aboutPage = join('pages', 'hmr', 'about7.js')
const aboutContent = await next.readFile(aboutPage)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
aboutPage,
aboutContent.replace(
'export default',
'export default undefined;\nexport const fn ='
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"The default export is not a React Component in page: "/hmr/about7""`
)
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await waitForNoRedbox(browser)
})
it('should recover after webpack parse error in an imported file', async () => {
const browser = await next.browser(basePath + '/hmr/about8')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about8.js'),
(content) =>
content.replace(
'export default',
'import "../../components/parse-error.xyz"\nexport default'
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Build Error')
if (process.env.IS_TURBOPACK_TEST) {
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"./components/parse-error.xyz
Unknown module type
This module doesn't have an associated type. Use a known file extension, or register a loader for it.
Read more: https://nextjs.org/docs/app/api-reference/next-config-js/turbo#webpack-loaders"
`)
} else if (process.env.NEXT_RSPACK) {
expect(trimEndMultiline(await getRedboxSource(browser)))
.toMatchInlineSnapshot(`
"./components/parse-error.xyz
× Module parse failed:
╰─▶ × JavaScript parse error: Expression expected
╭─[3:0]
1 │ This
2 │ is
3 │ }}}
· ─
4 │ invalid
5 │ js
╰────
help:
You may need an appropriate loader to handle this file type.
Import trace for requested module:
./components/parse-error.xyz
./pages/hmr/about8.js"
`)
} else {
expect(await getRedboxSource(browser)).toMatchInlineSnapshot(`
"./components/parse-error.xyz
Module parse failed: Unexpected token (3:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
| This
| is
> }}}
| invalid
| js
Import trace for requested module:
./components/parse-error.xyz
./pages/hmr/about8.js"
`)
}
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await waitForNoRedbox(browser)
})
it('should recover after loader parse error in an imported file', async () => {
const browser = await next.browser(basePath + '/hmr/about9')
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await next.patchFile(
join('pages', 'hmr', 'about9.js'),
(content) =>
content.replace(
'export default',
'import "../../components/parse-error.js"\nexport default'
),
async () => {
await waitForRedbox(browser)
expect(await getRedboxHeader(browser)).toMatch('Build Error')
let redboxSource = await getRedboxSource(browser)
redboxSource = redboxSource.replace(`${next.testDir}`, '.')
if (process.env.IS_TURBOPACK_TEST) {
expect(next.normalizeTestDirContent(redboxSource))
.toMatchInlineSnapshot(`
"./components/parse-error.js (3:1)
Expression expected
1 | This
2 | is
> 3 | }}}
| ^
4 | invalid
5 | js
Parsing ecmascript source code failed
Import traces:
Browser:
./components/parse-error.js
./pages/hmr/about9.js
SSR:
./components/parse-error.js
./pages/hmr/about9.js"
`)
} else if (process.env.NEXT_RSPACK) {
expect(trimEndMultiline(next.normalizeTestDirContent(redboxSource)))
.toMatchInlineSnapshot(`
"./components/parse-error.js
╰─▶ × Error: x Expression expected
│ ,-[3:1]
│ 1 | This
│ 2 | is
│ 3 | }}}
│ : ^
│ 4 | invalid
│ 5 | js
\`----
│ Caused by:
│ Syntax Error
Import trace for requested module:
./components/parse-error.js
./pages/hmr/about9.js"
`)
} else {
redboxSource = redboxSource.substring(
0,
redboxSource.indexOf('`----')
)
expect(next.normalizeTestDirContent(redboxSource))
.toMatchInlineSnapshot(`
"./components/parse-error.js
Error: x Expression expected
,-[3:1]
1 | This
2 | is
3 | }}}
: ^
4 | invalid
5 | js
"
`)
}
}
)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
await waitForNoRedbox(browser)
})
it('should recover from errors in getInitialProps in client', async () => {
const browser = await next.browser(basePath + '/hmr')
const erroredPage = join('pages', 'hmr', 'error-in-gip.js')
const errorContent = await next.readFile(erroredPage)
await browser.elementByCss('#error-in-gip-link').click()
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"an-expected-error-in-gip"`
)
await next.patchFile(
erroredPage,
(content) => content.replace('throw error', 'return {}'),
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
})
await next.patchFile(erroredPage, errorContent)
await retry(async () => {
await browser.refresh()
await waitFor(2000)
const text = await getBrowserBodyText(browser)
if (text.includes('Hello')) {
throw new Error('waiting')
}
return expect(await getRedboxSource(browser)).toMatch(
/an-expected-error-in-gip/
)
})
}
)
})
it('should recover after an error reported via SSR', async () => {
const browser = await next.browser(basePath + '/hmr/error-in-gip')
await waitForRedbox(browser)
expect(await getRedboxDescription(browser)).toMatchInlineSnapshot(
`"an-expected-error-in-gip"`
)
await next.patchFile(
join('pages', 'hmr', 'error-in-gip.js'),
(content) => content.replace('throw error', 'return {}'),
async () => {
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/Hello/)
})
}
)
await retry(async () => {
await browser.refresh()
await waitFor(2000)
const text = await getBrowserBodyText(browser)
if (text.includes('Hello')) {
throw new Error('waiting')
}
return expect(await getRedboxSource(browser)).toMatch(
/an-expected-error-in-gip/
)
})
})
if (!process.env.IS_TURBOPACK_TEST) {
it('should have client HMR events in trace file', async () => {
const traceData = await next.readFile(`${getDistDir()}/trace`)
expect(traceData).toContain('client-hmr-latency')
expect(traceData).toContain('client-error')
expect(traceData).toContain('client-success')
expect(traceData).toContain('client-full-reload')
})
}
}

View File

@@ -0,0 +1,91 @@
import { getRedboxHeader, retry } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
export function runFullReloadHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
it('should warn about full reload in cli output - anonymous page function', async () => {
const start = next.cliOutput.length
const browser = await next.browser(
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 next.browser(basePath + '/hmr/runtime-error')
const cliWarning =
'Fast Refresh had to perform a full reload due to a runtime error.'
await retry(async () => {
expect(await getRedboxHeader(browser)).toMatch(/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()
})
}

View File

@@ -0,0 +1,277 @@
import { join } from 'path'
import { getBrowserBodyText, retry, waitFor } from 'next-test-utils'
import { nextTestSetup } from 'e2e-utils'
export function runHotModuleReloadHmrTest(nextConfig: {
basePath: string
assetPrefix: string
}) {
const { next } = nextTestSetup({
files: __dirname,
nextConfig,
patchFileDelay: 500,
})
const { basePath } = nextConfig
describe('delete a page and add it back', () => {
it('should load the page properly', async () => {
const contactPagePath = join('pages', 'hmr', 'contact.js')
const newContactPagePath = join('pages', 'hmr', '_contact.js')
const browser = await next.browser(basePath + '/hmr/contact')
try {
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the contact page.')
expect(next.cliOutput).toMatch(/GET .*\/hmr\/contact 200/)
let cliOutputLength = next.cliOutput.length
// Rename the file to mimic a deleted page
await next.renameFile(contactPagePath, newContactPagePath)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This page could not be found/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toMatch(
/GET .*\/hmr\/contact 404/
)
cliOutputLength = next.cliOutput.length
// Rename the file back to the original filename
await next.renameFile(newContactPagePath, contactPagePath)
// wait until the page comes back
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the contact page/
)
})
expect(next.cliOutput.slice(cliOutputLength)).toMatch(
/GET .*\/hmr\/contact 200/
)
} finally {
await next
.renameFile(newContactPagePath, contactPagePath)
.catch(() => {})
}
})
})
describe('editing a page', () => {
it('should detect the changes and display it', async () => {
const browser = await next.browser(basePath + '/hmr/about')
const text = await browser.elementByCss('p').text()
expect(text).toBe('This is the about page.')
const aboutPagePath = join('pages', 'hmr', 'about.js')
const originalContent = await next.readFile(aboutPagePath)
const editedContent = originalContent.replace(
'This is the about page',
'COOL page'
)
// change the content
try {
await next.patchFile(aboutPagePath, editedContent)
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(/COOL page/)
})
} finally {
// add the original content
await next.patchFile(aboutPagePath, originalContent)
}
await retry(async () => {
expect(await getBrowserBodyText(browser)).toMatch(
/This is the about page/
)
})
})
it('should not reload unrelated pages', async () => {
const browser = await next.browser(basePath + '/hmr/counter')
const text = await browser
.elementByCss('button')
.click()
.elementByCss('button')
.click()
.elementByCss('p')
.text()
expect(text).toBe('COUNT: 2')
const aboutPagePath = join('pages', 'hmr', 'about.js')
const originalContent = await next.readFile(aboutPagePath)
const editedContent = originalContent.replace(
'This is the about page',
'COOL page'
)
try {
// Change the about.js page
await next.patchFile(aboutPagePath, editedContent)
// Check whether the this page has reloaded or not.
await retry(async () => {
expect(await browser.elementByCss('p').text()).toMatch(/COUNT: 2/)
})
} finally {
// restore the about page content.
await next.patchFile(aboutPagePath, originalContent)
}
})
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
// Also: https://github.com/vercel/styled-jsx/issues/425
it('should update styles correctly', async () => {
const browser = await next.browser(basePath + '/hmr/style')
const pTag = await browser.elementByCss('.hmr-style-page p')
const initialFontSize = await pTag.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const pagePath = join('pages', 'hmr', 'style.js')
const originalContent = await next.readFile(pagePath)
const editedContent = originalContent.replace('100px', '200px')
// Change the page
await next.patchFile(pagePath, editedContent)
try {
// Check whether the this page has reloaded or not.
await retry(async () => {
const editedPTag = await browser.elementByCss('.hmr-style-page p')
expect(await editedPTag.getComputedCss('font-size')).toBe('200px')
})
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
await next.patchFile(pagePath, originalContent)
}
})
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
// Also: https://github.com/vercel/styled-jsx/issues/425
it('should update styles in a stateful component correctly', async () => {
const browser = await next.browser(
basePath + '/hmr/style-stateful-component'
)
const pagePath = join('pages', 'hmr', 'style-stateful-component.js')
const originalContent = await next.readFile(pagePath)
try {
const pTag = await browser.elementByCss('.hmr-style-page p')
const initialFontSize = await pTag.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const editedContent = originalContent.replace('100px', '200px')
// Change the page
await next.patchFile(pagePath, editedContent)
// Check whether the this page has reloaded or not.
await retry(async () => {
const editedPTag = await browser.elementByCss('.hmr-style-page p')
expect(await editedPTag.getComputedCss('font-size')).toBe('200px')
})
} finally {
await next.patchFile(pagePath, originalContent)
}
})
// Added because of a regression in react-hot-loader, see issues: #4246 #4273
// Also: https://github.com/vercel/styled-jsx/issues/425
it('should update styles in a dynamic component correctly', async () => {
const browser = await next.browser(
basePath + '/hmr/style-dynamic-component'
)
const secondBrowser = await next.browser(
basePath + '/hmr/style-dynamic-component'
)
const pagePath = join('components', 'hmr', 'dynamic.js')
const originalContent = await next.readFile(pagePath)
try {
const div = await browser.elementByCss('#dynamic-component')
const initialClientClassName = await div.getAttribute('class')
const initialFontSize = await div.getComputedCss('font-size')
expect(initialFontSize).toBe('100px')
const initialHtml = await next.render(
basePath + '/hmr/style-dynamic-component'
)
expect(initialHtml.includes('100px')).toBeTruthy()
const $initialHtml = await next.render$(
basePath + '/hmr/style-dynamic-component'
)
const initialServerClassName =
$initialHtml('#dynamic-component').attr('class')
expect(initialClientClassName === initialServerClassName).toBeTruthy()
const editedContent = originalContent.replace('100px', '200px')
// Change the page
await next.patchFile(pagePath, editedContent)
// wait for 5 seconds
await waitFor(5000)
// Check whether the this page has reloaded or not.
const editedDiv = await secondBrowser.elementByCss('#dynamic-component')
const editedClientClassName = await editedDiv.getAttribute('class')
const editedFontSize = await editedDiv.getComputedCss('font-size')
const browserHtml = await secondBrowser.eval(
'document.documentElement.innerHTML'
)
expect(editedFontSize).toBe('200px')
expect(browserHtml.includes('font-size:200px')).toBe(true)
expect(browserHtml.includes('font-size:100px')).toBe(false)
const editedHtml = await next.render(
basePath + '/hmr/style-dynamic-component'
)
expect(editedHtml.includes('200px')).toBeTruthy()
const $editedHtml = await next.render$(
basePath + '/hmr/style-dynamic-component'
)
const editedServerClassName =
$editedHtml('#dynamic-component').attr('class')
expect(editedClientClassName === editedServerClassName).toBe(true)
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
await next.patchFile(pagePath, originalContent)
}
})
it('should not full reload when nonlatin characters are used', async () => {
const browser = await next.browser(basePath + '/hmr/nonlatin')
const pagePath = join('pages', 'hmr', 'nonlatin.js')
const originalContent = await next.readFile(pagePath)
try {
const timeOrigin = await browser.eval('performance.timeOrigin')
const editedContent = originalContent.replace(
'<div>テスト</div>',
'<div class="updated">テスト</div>'
)
// Change the page
await next.patchFile(pagePath, editedContent)
await browser.waitForElementByCss('.updated')
expect(await browser.eval('performance.timeOrigin')).toEqual(timeOrigin)
} finally {
// Finally is used so that we revert the content back to the original regardless of the test outcome
// restore the about page content.
await next.patchFile(pagePath, originalContent)
}
})
})
}