Commit Graph

5 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
Luke Sandberg dde2980379 Scope Safari ?ts= cache-buster to CSS/font assets only (Pages Router) (#92580)
### What?

Refactors the Safari `?ts=` cache-busting workaround in the Pages Router so it **only appears on CSS and font URLs**, not on script tags or script preload links.

### Why?

The `?ts=` timestamp was originally added to all preloaded assets as a workaround for a [Safari caching bug](https://bugs.webkit.org/show_bug.cgi?id=187726) (see [#5860](https://github.com/vercel/next.js/issues/5860)). When it appears on `<script>` tags, the Turbopack runtime's `getAssetSuffixFromScriptSrc()` reads the executing script's query string and infers it as the `ASSET_SUFFIX`, which then leaks onto all static asset URLs — including images. This causes `next/image` validation errors because the image URL gets an unexpected `?ts=` parameter.

Fixes #92118

### How?

Instead of maintaining parallel `assetQueryString` (with `?ts=`) and `scriptAssetQueryString` (without `?ts=`) paths, this PR:

1. **`assetQueryString`** carries only the deployment token (`?dpl=...`) and is used for all script-related URLs (script tags, script preloads)
2. **`safariCacheBuster`** is a new, separate field (`?ts=<timestamp>` or `""`) that is only combined with `assetQueryString` at the 3 CSS/font URL sites via a `joinQueryStrings()` helper
3. Removes the `scriptAssetQueryString` / `scriptMutableAssetQueryString` / `cssAssetQueryString` fields entirely

The Safari cache-buster is computed the same way as before (dev server only, Safari user-agent check) — it just no longer contaminates the base query string.

### Test plan

- [x] Updated existing test in `test/e2e/app-document/rendering.test.ts` to assert `?ts=` appears only on `<link rel="preload" as="style">` and `<link rel="preload" as="font">`, and does NOT appear on `<script>` or `<link rel="preload" as="script">`
- [x] `pnpm --filter=next types` passes

<!-- NEXT_JS_LLM_PR -->
2026-04-14 16:45:23 -07:00
Niklas Mischkulnig 1c1550ca7b tests: Assert dpl query string in all tests for Turbopack (#90592)
Enable skew protection for all start-mode tests when Turbopack is enabled (i.e. not dev). This works by setting `NEXT_DEPLOYMENT_ID` in the test infra

- When running the tests, `packages/next/src/server/lib/router-utils/resolve-routes.ts` now validates that all static asset requests have the correct `?dpl=...` query param value, and returns a 404 if missing.
- It can be disabled with `disableAutoSkewProtection: true` in the test options
- There were various tests that were asserting URLs and were missing the optional `?dpl` at the end.
   - `next.getDeploymentIdQuery()` can be used to get the `?dpl=dpl_123912js` string (or an empty string)
   - `next.getAssetQuery()` currently behaves like `getDeploymentIdQuery`, but will switch to prefer the immutable static token in #88607
2026-02-27 10:06:14 +01:00
Niklas Mischkulnig 48725b22c7 Reapply "Turbopack: layout segment optimization for Pages" (#77339) (#77696)
A clean revert of the revert #77339 of #74815

Closes PACK-3715
2025-04-02 16:09:07 -07:00
Niklas Mischkulnig ef5d8068c6 Port "app-document" test to e2e (#77748)
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:

## For Contributors

### Improving Documentation

- Run `pnpm prettier-fix` to fix formatting issues before opening the PR.
- Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide

### Adding or Updating Examples

- The "examples guidelines" are followed from our contributing doc https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md
- Make sure the linting passes by running `pnpm build && pnpm lint`. See https://github.com/vercel/next.js/blob/canary/contributing/repository/linting.md

### Fixing a bug

- Related issues linked using `fixes #number`
- Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md

### Adding a feature

- Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md


## For Maintainers

- Minimal description (aim for explaining to someone not on the team to understand the PR)
- When linking to a Slack thread, you might want to share details of the conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic behind a change

### What?

### Why?

### How?

Closes NEXT-
Fixes #

-->
2025-04-02 14:37:47 -07:00