Commit Graph

17 Commits

Author SHA1 Message Date
Jamiboy Mohammad 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 -->
2026-09-15 10:29:06 -07:00
Jiwon Choi 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 -->
2026-08-21 22:45:12 +02:00
Tim Neutkens 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 -->
2026-05-22 14:01:58 +02:00
Mitul Shah 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>
2026-03-04 12:49:57 +01:00
Sebastian "Sebbie" Silbermann 7d98e0b534 [test] Include error code in Redbox snapshot (#90497) 2026-02-26 11:01:25 +00:00
Jimmy Lai 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>
2026-01-05 13:30:06 +01:00
Zack Tanner 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
2025-10-27 12:59:45 -07:00
Hendrik Liebau 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>
2025-08-26 14:04:27 +02:00
Janka Uryga 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.
2025-07-31 21:07:06 +00:00
Sebastian "Sebbie" Silbermann 4618e3d902 Hide <anonymous> stackframes if sandwiched between two ignore-listed frames (#81067) 2025-07-14 11:41:04 +02:00
Sebastian "Sebbie" Silbermann 7ed2d23ae5 [devtools] Omit line/col numbers for anonymous sources (#81223) 2025-07-03 15:23:27 +02:00
Sebastian "Sebbie" Silbermann fdd7f76222 Reland "[Link] Add prefetch="auto" option" (#78821)
Co-authored-by: Andrew Clark <git@andrewclark.io>
2025-05-05 17:30:26 +02:00
Sebastian "Sebbie" Silbermann 8ea7d4bb35 [dev-overlay] Move error.name to label (#78198) 2025-04-25 11:00:58 +02:00
Sebastian "Sebbie" Silbermann b9d65b8afe [test] Assert on all errors in Redbox matchers (#77907) 2025-04-10 10:00:42 +02:00
Sebastian "Sebbie" Silbermann 855670e836 [dev-overlay] Remove "Unhandled Runtime Error" label (#77484) 2025-03-25 14:10:35 -07:00
Sebastian "Sebbie" Silbermann 86aef9d9bc Only log unrecoverable SSR shell errors once (#76484) 2025-02-25 14:45:10 +01:00
Sebastian "Sebbie" Silbermann 034972215c Consolidate next/link error tests (#76214) 2025-02-19 23:34:04 +01:00