mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
codex/fallback-root-cache
17 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
77fa82756a |
test: enable verified pages-router deploy tests (#98526)
## Summary
Enable the same 12 previously selected deployment-test scopes across 12
pages-router test files, now in a stack rooted on canary. Remove 11
`skipDeployment` options and their obsolete skip guards. Remove the
selected suite’s deploy-only placeholder return. Other mode, bundler,
middleware, and Cache Components exclusions remain in place.
This preserves the selection with passing evidence from the previous
deployment runs. No additional candidate scopes are enabled; excluded
variants are not counted as deployment coverage.
## Verification
- All selected test registration names and assertion bodies match the
previous enabled revision, checked by AST comparison.
- Verified that the canary diff contains only the inventoried exclusions
and their obsolete skip plumbing; other exclusions are preserved.
- Formatting and lint passed; 77 gate infrastructure unit tests passed.
- Full local bootstrap was blocked by missing package-level dependencies
in the temporary worktree. Fresh deployment execution on these rewritten
commits remains to be verified in CI.
<details>
<summary>Preserved scope inventory (12)</summary>
- ID 222: `test/e2e/404-page-custom-error/404-page-custom-error.test.ts`
— `describe('Default 404 Page with custom _error', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should respond to 404 correctly', async () => {
const res = await next.fetch('/404')
expect(res.status).toBe(404)
expect(await res.text()).toContain('This page could not be found')
})
it('should render error correctly', async () => {
const text = await next.render('/err')
expect(text).toContain(isNextDev ? 'oops' : 'Internal Server Error')
})
it('should render index page normal', async () => {
const html = await next.render('/')
expect(html).toContain('hello from index')
})
;(isNextStart ? it : it.skip)(
'should set pages404 in routes-manifest correctly',
async () => {
const data = JSON.parse(await
next.readFile('.next/routes-manifest.json'))
expect(data.pages404).toBe(true)
}
)
;(isNextStart ? it : it.skip)('should have output 404.html', async () =>
{
const pagesManifest = await next.readJSON(
'.next/server/pages-manifest.json'
)
const page = pagesManifest['/404']
expect(page.endsWith('.html')).toBe(true)
})
})`
- ID 223: `test/e2e/404-page/404-page.test.ts` — `describe('404 Page
Support', () => {
const { next } = nextTestSetup({
files: __dirname,
})
const gip404Err =
/`pages\/404` can not have getInitialProps\/getServerSideProps/
it('should use pages/404', async () => {
const html = await next.render('/abc')
expect(html).toContain('custom 404 page')
})
it('should set correct status code with pages/404', async () => {
const res = await next.fetch('/abc')
expect(res.status).toBe(404)
})
it('should use pages/404 for .d.ts file', async () => {
const html = await next.render('/invalidExtension')
expect(html).toContain('custom 404 page')
})
it('should not error when visited directly', async () => {
const res = await next.fetch('/404')
expect(res.status).toBe(404)
expect(await res.text()).toContain('custom 404 page')
})
it('should render _error for a 500 error still', async () => {
const html = await next.render('/err')
expect(html).not.toContain('custom 404 page')
expect(html).toContain(isNextDev ? 'oops' : 'Internal Server Error')
})
if (isNextStart) {
it('should output 404.html during build', async () => {
const manifest = await next.readJSON('.next/server/pages-manifest.json')
const page = manifest['/404']
expect(page.endsWith('.html')).toBe(true)
})
it('should still output 404.js anyway', async () => {
expect(await next.hasFile('.next/server/pages/404.js')).toBe(true)
})
it('should add /404 to pages-manifest correctly', async () => {
const manifest = await next.readJSON('.next/server/pages-manifest.json')
expect('/404' in manifest).toBe(true)
})
}
if (isNextDev) {
it('falls back to _error correctly without pages/404', async () => {
const original404 = await next.readFile('pages/404.js')
try {
await next.deleteFile('pages/404.js')
await retry(async () => {
const res = await next.fetch('/abc')
expect(res.status).toBe(404)
expect(await res.text()).toContain('This page could not be found')
})
} finally {
await next.patchFile('pages/404.js', original404)
}
})
it('shows error with getInitialProps in pages/404 dev', async () => {
const original404 = await next.readFile('pages/404.js')
try {
await next.patchFile(
'pages/404.js',
`
const page = () => 'custom 404 page'
page.getInitialProps = () => ({ a: 'b' })
export default page
`
)
await next.render('/abc')
await retry(async () => {
expect(next.cliOutput).toMatch(gip404Err)
})
} finally {
await next.patchFile('pages/404.js', original404)
}
})
it('does not show error with getStaticProps in pages/404 dev', async ()
=> {
const original404 = await next.readFile('pages/404.js')
const getOutput = next.getCliOutputFromHere()
try {
await next.patchFile(
'pages/404.js',
`
const page = () => 'custom 404 page'
export const getStaticProps = () => ({ props: { a: 'b' } })
export default page
`
)
await next.render('/abc')
await retry(async () => {
const html = await next.render('/abc')
expect(html).toContain('custom 404 page')
})
expect(getOutput()).not.toMatch(gip404Err)
} finally {
await next.patchFile('pages/404.js', original404)
}
})
it('shows error with getServerSideProps in pages/404 dev', async () => {
const original404 = await next.readFile('pages/404.js')
try {
await next.patchFile(
'pages/404.js',
`
const page = () => 'custom 404 page'
export const getServerSideProps = () => ({ props: { a: 'b' } })
export default page
`
)
await next.render('/abc')
await retry(async () => {
expect(next.cliOutput).toMatch(gip404Err)
})
} finally {
await next.patchFile('pages/404.js', original404)
}
})
}
})`
- ID 225: `test/e2e/500-page/500-page.test.ts` — `describe('500 Page
Support', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should use pages/500', async () => {
const html = await next.render('/500')
expect(html).toContain('custom 500 page')
})
it('should set correct status code with pages/500', async () => {
const res = await next.fetch('/500')
expect(res.status).toBe(500)
})
it('should not error when visited directly', async () => {
const res = await next.fetch('/500')
expect(res.status).toBe(500)
expect(await res.text()).toContain('custom 500 page')
})
if (isNextStart) {
it('should output 500.html during build', async () => {
const manifest = await next.readJSON('.next/server/pages-manifest.json')
const page = manifest['/500']
expect(page.endsWith('.html')).toBe(true)
})
it('should add /500 to pages-manifest correctly', async () => {
const manifest = await next.readJSON('.next/server/pages-manifest.json')
expect('/500' in manifest).toBe(true)
})
}
if (isNextDev) {
it('shows error with getInitialProps in pages/500 dev', async () => {
const original500 = await next.readFile('pages/500.js')
try {
await next.patchFile(
'pages/500.js',
`
const page = () => 'custom 500 page'
page.getInitialProps = () => ({ a: 'b' })
export default page
`
)
await next.render('/500')
await retry(async () => {
expect(next.cliOutput).toMatch(
/`pages\/500` can not have getInitialProps\/getServerSideProps/
)
})
} finally {
await next.patchFile('pages/500.js', original500)
}
})
it('does not show error with getStaticProps in pages/500 dev', async ()
=> {
const original500 = await next.readFile('pages/500.js')
const outputBefore = next.cliOutput.length
try {
await next.patchFile(
'pages/500.js',
`
const page = () => 'custom 500 page'
export const getStaticProps = () => ({ props: { a: 'b' } })
export default page
`
)
await next.render('/abc')
await retry(async () => {
expect(next.cliOutput.slice(outputBefore)).not.toMatch(
/`pages\/500` can not have getInitialProps\/getServerSideProps/
)
})
} finally {
await next.patchFile('pages/500.js', original500)
}
})
it('shows error with getServerSideProps in pages/500 dev', async () => {
const original500 = await next.readFile('pages/500.js')
try {
await next.patchFile(
'pages/500.js',
`
const page = () => 'custom 500 page'
export const getServerSideProps = () => ({ props: { a: 'b' } })
export default page
`
)
await next.render('/500')
await retry(async () => {
expect(next.cliOutput).toMatch(
/`pages\/500` can not have getInitialProps\/getServerSideProps/
)
})
} finally {
await next.patchFile('pages/500.js', original500)
}
})
}
})`
- ID 227:
`test/e2e/api-resolver-query-writeable/api-resolver-query-writeable.test.ts`
— `describe('api-resolver-query-writeable', () => {
const { next } = nextTestSetup({
files: __dirname,
startCommand: 'node server.js',
serverReadyPattern: /Next mode: (production|development)/,
dependencies: {
'get-port': '5.1.1',
express: '5.1.0',
},
})
it('should allow req.query to be writable and reflect changes made in
the API handler', async () => {
const res = await next.fetch('/api?hello=yes', {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
})
if (!res.ok) {
throw new Error('Fetch failed')
}
const data = await res.json()
expect(data).toEqual({ query: { hello: 'yes', changed: 'yes' } })
})
})`
- ID 230: `test/e2e/app-document/client.test.ts` — `describe('Document
and App - Client side', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('should share module state with pages', async () => {
const browser = await next.browser('/shared')
const text = await browser.elementByCss('#currentstate').text()
expect(text).toBe('UPDATED CLIENT')
})
if (isNextDev) {
it('should detect the changes to pages/_app.js and display it', async ()
=> {
const appPath = 'pages/_app.js'
const originalContent = await next.readFile(appPath)
try {
const browser = await next.browser('/')
const text = await browser.elementByCss('#hello-hmr').text()
expect(text).toBe('Hello HMR')
// change the content
const editedContent = originalContent.replace('Hello HMR', 'Hi HMR')
await next.patchFile(appPath, editedContent)
await retry(async () =>
expect(await browser.elementByCss('body').text()).toContain('Hi HMR')
)
// add the original content
await next.patchFile(appPath, originalContent)
await retry(async () =>
expect(await browser.elementByCss('body').text()).toContain(
'Hello HMR'
)
)
} finally {
await next.patchFile(appPath, originalContent)
}
})
it('should detect the changes to pages/_document.js and display it',
async () => {
const appPath = 'pages/_document.js'
const originalContent = await next.readFile(appPath)
try {
const browser = await next.browser('/')
const text = await browser.elementByCss('#hello-hmr').text()
expect(text).toBe('Hello HMR')
const editedContent = originalContent.replace(
'Hello Document HMR',
'Hi Document HMR'
)
// change the content
await next.patchFile(appPath, editedContent)
await retry(async () =>
expect(await browser.elementByCss('body').text()).toContain(
'Hi Document HMR'
)
)
// add the original content
await next.patchFile(appPath, originalContent)
await retry(async () =>
expect(await browser.elementByCss('body').text()).toContain(
'Hello Document HMR'
)
)
} finally {
await next.patchFile(appPath, originalContent)
}
})
it('should keep state between page navigations', async () => {
const browser = await next.browser('/')
const randomNumber = await browser.elementByCss('#random-number').text()
const switchedRandomNumer = await browser
.elementByCss('#about-link')
.click()
.waitForElementByCss('.page-about')
.elementByCss('#random-number')
.text()
expect(switchedRandomNumer).toBe(randomNumber)
await browser.close()
})
}
})`
- ID 250: `test/e2e/disable-js/disable-js.test.ts` — `describe('disabled
runtime JS', () => {
const { next, isNextDev, isNextStart } = nextTestSetup({
files: __dirname,
})
it('should render the page', async () => {
const html = await next.render('/')
expect(html).toMatch(/Hello World/)
})
it('should not have __NEXT_DATA__ script', async () => {
const html = await next.render('/')
const $ = cheerio.load(html)
if (isNextStart) {
expect($('script#__NEXT_DATA__').length).toBe(0)
}
if (isNextDev) {
expect($('script#__NEXT_DATA__').length).toBe(1)
}
})
if (isNextStart) {
it('should not have scripts', async () => {
const html = await next.render('/')
const $ = cheerio.load(html)
expect($('script[src]').length).toBe(0)
})
it('should not have preload links', async () => {
const html = await next.render('/')
const $ = cheerio.load(html)
expect($('link[rel=preload]').length).toBe(0)
})
}
if (isNextDev) {
it('should have a script for each preload link', async () => {
const html = await next.render('/')
const $ = cheerio.load(html)
const preloadLinks = $('link[rel=preload]')
preloadLinks.each((idx, element) => {
const url = $(element).attr('href')
expect($(`script[src="${url}"]`).length).toBe(1)
})
})
}
})`
- ID 255: `test/e2e/gip-identifier/gip-identifier.test.ts` —
`describe('gip identifiers', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
const getNextData = async () => {
const html = await next.render('/')
const $ = cheerio.load(html)
return JSON.parse($('#__NEXT_DATA__').text())
}
it('should not have gip or appGip in NEXT_DATA for page without
getInitialProps', async () => {
const data = await getNextData()
expect(data.gip).toBe(undefined)
expect(data.appGip).toBe(undefined)
})
if (isNextDev) {
it('should have gip in NEXT_DATA for page with getInitialProps', async
() => {
await next.patchFile(
'pages/index.js',
`
const Page = () => 'hi'
Page.getInitialProps = () => ({ hello: 'world' })
export default Page
`
)
await retry(async () => {
const data = await getNextData()
expect(data.gip).toBe(true)
})
})
it('should have gip and appGip in NEXT_DATA for page with
getInitialProps and _app with getInitialProps', async () => {
await next.patchFile(
'pages/_app.js',
`
const App = ({ Component, pageProps }) => <Component {...pageProps} />
App.getInitialProps = async (ctx) => {
let pageProps = {}
if (ctx.Component.getInitialProps) {
pageProps = await ctx.Component.getInitialProps(ctx.ctx)
}
return { pageProps }
}
export default App
`
)
await retry(async () => {
const data = await getNextData()
expect(data.gip).toBe(true)
expect(data.appGip).toBe(true)
})
})
it('should only have appGip in NEXT_DATA for page without
getInitialProps and _app with getInitialProps', async () => {
await next.patchFile('pages/index.js', `export default () => 'hi'\n`)
await retry(async () => {
const data = await getNextData()
expect(data.gip).toBe(undefined)
expect(data.appGip).toBe(true)
})
})
}
})`
- ID 258:
`test/e2e/i18n-data-fetching-redirect/redirect-from-context.test.ts` —
`describe('i18n-data-fetching-redirect', () => {
const { next } = nextTestSetup({
files: {
pages: new FileRef(join(__dirname, 'app/pages')),
'next.config.js': new FileRef(join(__dirname, 'app/next.config.js')),
},
dependencies: {},
})
describe('Redirect to locale from context', () => {
test.each`
path | locale
${'gssp-redirect'} | ${'en'}
${'gssp-redirect'} | ${'sv'}
${'gsp-blocking-redirect'} | ${'en'}
${'gsp-blocking-redirect'} | ${'sv'}
${'gsp-fallback-redirect'} | ${'en'}
${'gsp-fallback-redirect'} | ${'sv'}
`('$path $locale', async ({ path, locale }) => {
const browser = await next.browser(`/${locale}/${path}/from-ctx`)
await check(
() => browser.eval('window.location.pathname'),
`/${locale}/home`
)
expect(await browser.elementByCss('#router-locale').text()).toBe(locale)
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/home'
)
expect(await
browser.elementByCss('#router-as-path').text()).toBe('/home')
})
test.each`
path | locale
${'gssp-redirect'} | ${'en'}
${'gssp-redirect'} | ${'sv'}
${'gsp-blocking-redirect'} | ${'en'}
${'gsp-blocking-redirect'} | ${'sv'}
${'gsp-fallback-redirect'} | ${'en'}
${'gsp-fallback-redirect'} | ${'sv'}
`('next/link $path $locale', async ({ path, locale }) => {
const browser = await next.browser(`/${locale}`)
await browser.eval('window.beforeNav = 1')
await browser.elementByCss(`#to-${path}-from-ctx`).click()
await check(
() => browser.eval('window.location.pathname'),
`/${locale}/home`
)
expect(await browser.eval('window.beforeNav')).toBe(1)
expect(await browser.elementByCss('#router-locale').text()).toBe(locale)
expect(await browser.elementByCss('#router-pathname').text()).toBe(
'/home'
)
expect(await
browser.elementByCss('#router-as-path').text()).toBe('/home')
})
})
})`
- ID 261:
`test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts`
— `describe('i18n-ignore-rewrite-source-locale with basepath', () => {
const { next } = nextTestSetup({
files: __dirname,
})
test.each(locales)(
'get public file by skipping locale in rewrite, locale: %s',
async (locale) => {
const res = await renderViaHTTP(
next.url,
`/basepath${locale}/rewrite-files/file.txt`
)
expect(res).toContain('hello from file.txt')
}
)
test.each(locales)(
'call api by skipping locale in rewrite, locale: %s',
async (locale) => {
const res = await renderViaHTTP(
next.url,
`/basepath${locale}/rewrite-api/hello`
)
expect(res).toContain('hello from api')
}
)
// build artifacts aren't available on deploy
if (!(global as any).isNextDeploy) {
// chunks are not written to disk with TURBOPACK
;(process.env.IS_TURBOPACK_TEST ? it.skip.each : it.each)(locales)(
'get _next/static/ files by skipping locale in rewrite, locale: %s',
async (locale) => {
const chunks = (
await fs.readdir(
path.join(next.testDir, next.distDir, 'static', 'chunks')
)
).filter((f) => f.endsWith('.js'))
await Promise.all(
chunks.map(async (file) => {
const res = await fetchViaHTTP(
next.url,
`/basepath${locale}/rewrite-files/_next/static/chunks/${file}`
)
// eslint-disable-next-line jest/no-standalone-expect
expect(res.status).toBe(200)
})
)
}
)
}
})`
- ID 267: `test/e2e/legacy-link-behavior/index.test.ts` —
`describe('Link with legacyBehavior', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
describe('if the child is an <a> tag', () => {
it('forwards the href attribute', async () => {
const $ = await next.render$('/')
const $a = $('a[href="/about"]')
expect($a.text()).toBe('About')
expect($a.attr('href')).toBe('/about')
})
it('navigates correctly', async () => {
const browser = await next.browser('/')
await browser.elementByCss('a[href="/about"]').click()
const title = await browser.elementByCss('#about-page').text()
expect(title).toBe('About Page')
})
})
it('works if the child is a number', async () => {
const browser = await next.browser('/child-is-a-number')
await browser.elementByCss('a[href="/about"]').click()
const title = await browser.elementByCss('h1').text()
expect(title).toBe('About Page')
})
it('works if the child is a string', async () => {
const browser = await next.browser('/child-is-a-string')
await browser.elementByCss('a[href="/about"]').click()
const title = await browser.elementByCss('h1').text()
expect(title).toBe('About Page')
})
it('errors when calling onClick without the event', async () => {
const browser = await next.browser('/invalid-onclick')
expect(await browser.elementByCss('#errors').text()).toBe('0')
await browser.elementByCss('#custom-button').click()
expect(await browser.elementByCss('#errors').text()).toBe('1')
})
it('should show a deprecation warning', async () => {
const browser = await next.browser('/')
await retry(async () => {
const logs = await browser.log()
const errors = logs.filter((log) => log.source === 'error')
if (isNextDev) {
expect(errors).toEqual([
{
message:
'`legacyBehavior` is deprecated and will be removed in a future release.
A codemod is available to upgrade your components:\n\n' +
'npx @next/codemod@latest new-link .\n\n' +
'Learn more:
https://nextjs.org/docs/app/building-your-application/upgrading/codemods#remove-a-tags-from-link-components',
source: 'error',
},
])
} else {
expect(errors).toEqual([])
}
})
})
describe('passHref', () => {
const expectHrefToBeForwardedInSSR = async (path: string) => {
const $ = await next.render$(path)
const $a = $('a[href="/about"]')
expect($a.text()).toBe('About')
expect($a.attr('href')).toBe('/about')
}
const expectLinkClickToNavigate = async (path: string) => {
const browser = await next.browser(path)
if (isNextDev) {
// We expect a deprecation warning (in a collapsed redbox), but no other
errors (e.g. no errors thrown by Link)
await openRedbox(browser)
expect(await createRedboxSnapshot(browser, next)).toEqual(
expect.objectContaining<Partial<ErrorSnapshot>>({
label: 'Console Error',
description: expect.stringContaining(
`\`legacyBehavior\` is deprecated and will be removed in a future
release.`
),
})
)
await browser.locateRedbox().press('Escape') // Close redbox so we can
click the link
}
await browser.elementByCss('a[href="/about"]').click()
const title = await browser.elementByCss('h1').text()
expect(title).toBe('About Page')
}
describe('with no prefech config', () => {
it('forwards the href attribute', async () => {
await expectHrefToBeForwardedInSSR('/passHref/default')
})
it('navigates correctly (failing)', async () => {
if (isNextDev) {
// FIXME(NAR-876): false positive due to debug info blocking the child
// await expectLinkClickToNavigate('/passHref/default')
const browser = await next.browser('/passHref/default')
await expect(browser).toDisplayRedbox(`
{
"description": "\`<Link legacyBehavior>\` received a direct child that
is either a Server Component, or JSX that was loaded with React.lazy().
This is not supported. Either remove legacyBehavior, or make the direct
child a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/passHref/default/page.tsx (7:7) @ Page
> 7 | <Link href="/about" legacyBehavior passHref>
| ^",
"stack": [
"Page app/passHref/default/page.tsx (7:7)",
],
}
`)
} else {
await expectLinkClickToNavigate('/passHref/default')
}
})
})
describe('with runtime prefetch', () => {
it('forwards the href attribute', async () => {
await expectHrefToBeForwardedInSSR('/passHref/runtime')
})
it('navigates correctly (failing)', async () => {
if (isNextDev) {
// FIXME(NAR-876): false positive due to debug info blocking the child
// await expectLinkClickToNavigate('/passHref/runtime')
const browser = await next.browser('/passHref/runtime')
await expect(browser).toDisplayRedbox(`
{
"description": "\`<Link legacyBehavior>\` received a direct child that
is either a Server Component, or JSX that was loaded with React.lazy().
This is not supported. Either remove legacyBehavior, or make the direct
child a Client Component that renders the Link's \`<a>\` tag.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/passHref/runtime/page.tsx (9:7) @ Page
> 9 | <Link href="/about" legacyBehavior passHref>
| ^",
"stack": [
"Page app/passHref/runtime/page.tsx (9:7)",
],
}
`)
} else {
await expectLinkClickToNavigate('/passHref/runtime')
}
})
})
describe('in dynamic code', () => {
it('forwards the href attribute', async () => {
await expectHrefToBeForwardedInSSR('/passHref/dynamic')
})
it('navigates correctly', async () => {
await expectLinkClickToNavigate('/passHref/dynamic')
})
})
})
})`
- ID 270: `test/e2e/next-link-errors/next-link-errors.test.ts` —
`describe('next-link', () => {
const { next, isNextDev } = nextTestSetup({
files: __dirname,
})
it('errors on invalid href', async () => {
const browser = await next.browser('/invalid-href')
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"description": "Failed prop type: The prop \`href\` expects a \`string\`
or \`object\` in \`<Link>\`, but got \`undefined\` instead.
Open your browser's console to view the Component stack trace.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/invalid-href/page.js (6:10) @ Hello
> 6 | return <Link>Hello, Dave!</Link>
| ^",
"stack": [
"Hello app/invalid-href/page.js (6:10)",
],
}
`)
}
// Client errors show "This page couldn\u2019t load"
expect(await browser.elementByCss('body').text()).toContain(
'This page couldn\u2019t load'
)
})
it('invalid `prefetch` causes runtime error (dev-only)', async () => {
const browser = await next.browser('/invalid-prefetch')
if (isNextDev) {
await expect(browser).toDisplayRedbox(`
{
"description": "Failed prop type: The prop \`prefetch\` expects a
\`boolean | "auto"\` in \`<Link>\`, but got \`string\` instead.
Open your browser's console to view the Component stack trace.",
"environmentLabel": null,
"label": "Runtime Error",
"source": "app/invalid-prefetch/page.js (7:5) @ Hello
> 7 | <Link prefetch="unknown" href="https://nextjs.org/">
| ^",
"stack": [
"Hello app/invalid-prefetch/page.js (7:5)",
],
}
`)
// Client errors show "This page couldn\u2019t load"
expect(await browser.elementByCss('body').text()).toContain(
'This page couldn\u2019t load'
)
} else {
expect(await browser.elementByCss('body').text()).toMatchInlineSnapshot(
`"Link with unknown \`prefetch\` renders in prod."`
)
}
})
})`
- ID 273: `test/e2e/pages-performance-mark/index.test.ts` —
`describe('pages performance mark', () => {
const { next } = nextTestSetup({
files: __dirname,
})
it('should render the page correctly without crashing with performance
mark', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('h1').text()).toBe('home')
})
})`
</details>
<details>
<summary>Deployment evidence for the additional scopes</summary>
- `test/e2e/404-page-custom-error/404-page-custom-error.test.ts` —
`describe('Default 404 Page with custom _error', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905641),
[cache](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905524).
- `test/e2e/404-page/404-page.test.ts` — `describe('404 Page Support',
() => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905631),
[cache](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905599).
-
`test/e2e/i18n-ignore-rewrite-source-locale/rewrites-with-basepath.test.ts`
— `describe('i18n-ignore-rewrite-source-locale with basepath', () => {`:
[normal](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905696),
[cache](https://github.com/vercel/next.js/actions/runs/34426785205/job/102715905562).
</details>
<!-- NEXT_JS_LLM -->
|
||
|
|
d7aa66c345 |
Remove generated error codes (#97687)
### Why? Should come up with better solution that does not block PRs with git conflict x-ref: https://vercel.slack.com/archives/C02CDC2ALJH/p1785263902728189?thread_ts=1785263687.502649&cid=C02CDC2ALJH ### How? - Delete `errors.json`, the error-code SWC plugin, generated WASM, merge driver, and validation/build tooling. - Stop attaching error codes to server-rendering digests, redboxes, and telemetry; native `Error.code` and `Error.name` remain available where applicable. - Remove the development-overlay error feedback UI, middleware, and telemetry event that depended on stable codes. - Update fixtures, snapshots, and guidance for code-free errors and numeric-only digests. <!-- NEXT_JS_LLM --> |
||
|
|
e860cec656 |
test: migrate webdriver callers to next.browser (#93941)
### What? Migrate remaining direct `next-webdriver` test callers that have a `NextInstance` to `next.browser()`, and expose the shared `Playwright` browser type from `e2e-utils`. ### Why? `NextInstance.browser` should be the supported browser-opening interface for test fixtures, with `next-webdriver` kept as the private implementation detail. ### How? Updated affected development, e2e, and production tests to call `next.browser()` directly, passing `baseUrl` where tests intentionally target a manually spawned or proxied server. Shared helpers now receive browser callbacks from the test context, and browser types import `Playwright` from `e2e-utils` instead of deriving from `next.browser` or importing from private paths. <!-- NEXT_JS_LLM_PR --> |
||
|
|
ad3296b9d4 |
Update default error pages (#90469)
Redesign the https://github.com/vercel/next.js/pull/87988 error pages to be more aligned with Next.js aesthetic, along with copy to be sharper. | Client | Server | |--------|--------| | <img width="4992" height="2830" alt="CleanShot 2026-03-03 at 17 26 07@2x" src="https://github.com/user-attachments/assets/f42bee3e-2ce8-44dc-9e26-79b81cf965d8" /> | <img width="4992" height="2830" alt="CleanShot 2026-03-03 at 17 23 38@2x" src="https://github.com/user-attachments/assets/19a596c6-6da7-4a42-9b53-16e3c2e6f867" /> | | <img width="4992" height="2830" alt="CleanShot 2026-03-03 at 17 25 22@2x" src="https://github.com/user-attachments/assets/a3075d79-c76a-4fbf-af6e-d4a44f1434eb" /> | <img width="4992" height="2830" alt="CleanShot 2026-03-03 at 17 24 17@2x" src="https://github.com/user-attachments/assets/5811cc98-f8e4-4451-bf9c-501548292245" /> | --------- Co-authored-by: Jimmy Lai <laijimmy0@gmail.com> Co-authored-by: Tim Neutkens <tim@timneutkens.nl> |
||
|
|
7d98e0b534 | [test] Include error code in Redbox snapshot (#90497) | ||
|
|
c74cb2ae25 |
Redesign default error pages with cleaner, more user-friendly UI (#87988)
- redesigns the default global + default error boundaries. removing the WSOD forever :/ - adds dark mode support - unifies a bit of the styling - add the digest for the server error - adds a return button on the client errors <img width="3328" height="1914" alt="CleanShot 2026-01-02 at 09 46 36@2x" src="https://github.com/user-attachments/assets/9994eb37-c22b-4847-9a18-e9acf0ae6236" /> <img width="2144" height="1242" alt="CleanShot 2026-01-02 at 09 47 01@2x" src="https://github.com/user-attachments/assets/9b3bcf39-63f6-4c8d-85c5-2976c25f5b7b" /> <img width="2238" height="1632" alt="CleanShot 2026-01-02 at 09 47 17@2x" src="https://github.com/user-attachments/assets/436433d7-9873-496a-9c96-536e070be9d7" /> <img width="1708" height="1260" alt="CleanShot 2026-01-02 at 09 47 23@2x" src="https://github.com/user-attachments/assets/1ec42402-dbd9-41a3-801b-4745a4f128f7" /> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> |
||
|
|
5e282808c0 |
remove unstable_forceStale prefetch option & restore prefetch={true} functionality (#85411)
`prefetch={true}` with `cacheComponents` was opting into runtime
prefetching, with `unstable_forceStale` preserving the old behavior of
fetching the full (stale) data. However we only intended to expose
runtime prefetching via the `export const prefetch` segment config.
This removes `unstable_forceStale` and restores previous
`prefetch={true}` behavior. I've disabled some of the tests that were
relying on the old link behavior for opting into runtime prefetching -
we'll need to refactor those to use the segment opt-in or just remove
them all together.
Fixes #85162
Closes NAR-320
|
||
|
|
4b66771895 |
Remove deprecated legacyBehavior and passHref prop from Link component (#83003)
The `legacyBehavior` prop of the `Link` component has been deprecated since #77473. For Next.js 16, we're finally removing support for it. Consequently, we're also removing support for the `passHref` prop, which was only useful in conjunction with the `legacyBehavior` prop. A [codemod is available](https://nextjs.org/docs/app/guides/upgrading/codemods#new-link) to help you automatically upgrade your codebase. reverts #77473 --------- Co-authored-by: Sebastian Sebbie Silbermann <sebastian.silbermann@vercel.com> |
||
|
|
1c5f64b993 |
[Cache Components] Runtime prefetching (#81088)
Initial implementation of runtime prefetching.
A "runtime prefetch" is a more complete version of a static prefetch
(i.e. the one that a link does by default), rendered on demand, and not
cached server-side. It will render the server part of the page
dynamically, allowing the usage of
- `params` and `searchParams`
- `cookies()`
- `"use cache: private"` (these are omitted from static prerenders)
- `"use cache"` with a short expire time (these are omitted from static
prerenders)
The result may be partial (in the PPR sense). It will exclude any parts
of the page that depend on
- uncached IO
- `connection()`, `headers()`
This allows the client router to cache the result, because it has a
well-defined stale time. Note that public caches with a stale time below
a certain fixed treshold will also be excluded, because it wouldn't make
sense to keep them around in the router cache if we need to throw them
away soon after getting them.
With this PR, `<Link prefetch={true}>` changes meaning
if `clientSegmentCache` + `cacheComponents` are enabled. It will now
initiate a runtime prefetch instead of a "full" prefetch, which included
everything that a navigation request would. Full prefetches can be done
via `<Link prefetch="unstable_forceStale">`. If only one of the two
flags is on, the behavior of `<Link prefetch={true}>` is unchanged from
how it currently works.
I've split the changes up into separate commits for ease of review:
1. Introducing the new workUnitStore type
2. Server - handling the prefetch header and rendering
3. Client - Link and segment cache changes
### Implementation notes
The client router sends `next-router-prefetch: 2` to signal that it
wants a runtime prefetch (as opposed to the old `next-router-prefetch:
1`, which is used for static prefetches).
> NOTE: this builder change is required for this to work on vercel
https://github.com/vercel/vercel/pull/13547. It was released in
`vercel@44.6.5`
Somewhat confusingly, in order to to avoid existing static prefetch
codepaths, we need the server to _not_ treat this as "a prefetch
request". Instead, we want to mostly treat this like we would a
navigation request, and render dynamically. This means that:
- `isPrefetchRequest` (from `parseRequestHeaders` in `app-render`) will
be `false`
- `getRequestMeta(req, 'isPrefetchRSCRequest')` won't be set
This is a bit ugly but it works for now. I'll try to clean it up in the
future.
We render a payload of the same shape as a navigation request (including
omitting shared layouts, as instructed by the `Next-Router-State-Tree`
header). But unlike a navigation request, we do a cache-components-style
prerender at runtime in order to exclude uncached/sync IO.
This prerender uses a new workUnitStore type, `'prerender-runtime'`.
This store type changes the behavior of `cookies()`, `params`,
`searchParams`, `"use cache: private"`, `next/root-params`, and others.
Unlike a static prerender, if we detect a bad uncached/sync IO usage, we
just log an error (instead of throwing it and erroring) and respond with
whatever we managed to render up until the render was aborted, in hopes
that we can still return something useful to the client. This request is
happening at runtime, so we should try to handle errors gracefully.
We track whether or not the prerender has any dynamic holes, and if it
does, set `x-nextjs-postponed: 1` on the response. This tells the client
router if we still need to fetch more data when navigating, or if we can
skip it because we already have a complete page. Ideally, we'd track
this information per-segment for better reuse on the client side, but
that's not in scope for this PR.
We also set the `x-nextjs-staletime` header on the response to tell the
client router how long it should keep this prefetch in the cache. Note
that this does not affect `Cache-Control`, which should still be the
same as a dynamic navigation request to prevent it from being cached by
anything other than the client router.
This may be improved in the future if it turns out we can safely set an
appropriate `Cache-Control: private, ...` that also accounts for e.g.
changing cookie values, but i'm erring on the side of caution for now.
|
||
|
|
4618e3d902 | Hide <anonymous> stackframes if sandwiched between two ignore-listed frames (#81067) | ||
|
|
7ed2d23ae5 | [devtools] Omit line/col numbers for anonymous sources (#81223) | ||
|
|
fdd7f76222 |
Reland "[Link] Add prefetch="auto" option" (#78821)
Co-authored-by: Andrew Clark <git@andrewclark.io> |
||
|
|
8ea7d4bb35 |
[dev-overlay] Move error.name to label (#78198)
|
||
|
|
b9d65b8afe | [test] Assert on all errors in Redbox matchers (#77907) | ||
|
|
855670e836 | [dev-overlay] Remove "Unhandled Runtime Error" label (#77484) | ||
|
|
86aef9d9bc | Only log unrecoverable SSR shell errors once (#76484) | ||
|
|
034972215c |
Consolidate next/link error tests (#76214)
|