next.js/test/e2e/app-dir/app-fetch-deduping/app-fetch-deduping.test.ts
app-fetch-deduping.test.ts164 lines4.8 KB
import { findPort, retry } from 'next-test-utils'
import http from 'http'
import { outdent } from 'outdent'
import { isNextDev, isNextStart, nextTestSetup } from 'e2e-utils'

describe('app-fetch-deduping', () => {
  if (isNextStart) {
    describe('during static generation', () => {
      const { next } = nextTestSetup({ files: __dirname, skipStart: true })
      let externalServerPort: number
      let externalServer: http.Server
      let successfulRequests = []

      beforeAll(async () => {
        externalServerPort = await findPort()
        externalServer = http.createServer((req, res) => {
          const parsedUrl = new URL(
            req.url,
            `http://localhost:${externalServerPort}`
          )
          const overrideStatus = parsedUrl.searchParams.get('status')

          // if the requested url has a "status" search param, override the response status
          if (overrideStatus) {
            res.statusCode = Number(overrideStatus)
          } else {
            successfulRequests.push(req.url)
          }

          // Generate a response with more than two MB of data.
          res.end(
            `Request ${req.url} received at ${Date.now()}\n\n${'a'.repeat(2_000_000)}`
          )
        })

        await new Promise<void>((resolve, reject) => {
          externalServer.listen(externalServerPort, () => {
            resolve()
          })

          externalServer.once('error', (err) => {
            reject(err)
          })
        })

        await next.patchFile(
          'next.config.js',
          `module.exports = {
            env: { TEST_SERVER_PORT: "${externalServerPort}" },
          }`
        )

        await next.build()
      })

      afterAll(() => externalServer.close())

      it('dedupes requests amongst static workers', async () => {
        expect(successfulRequests.length).toBe(1)
      })

      it('does not print a fetch cache size limit warning', async () => {
        expect(next.cliOutput).not.toInclude('Failed to set Next.js data cache')
      })
    })
  } else if (isNextDev) {
    describe('during next dev', () => {
      const { next } = nextTestSetup({ files: __dirname, patchFileDelay: 500 })
      function invocation(cliOutput: string): number {
        return cliOutput.match(/Route Handler invoked/g).length
      }

      it('should dedupe requests called from the same component', async () => {
        await next.patchFile(
          'app/test/page.tsx',
          outdent`
          async function getTime() {
            const res = await fetch("http://localhost:${next.appPort}/api/time")
            return res.text()
          }
          
          export default async function Home() {
            await getTime()
            await getTime()
            const time = await getTime()
          
            return <h1>{time}</h1>
          }`
        )

        await next.render('/test')

        expect(invocation(next.cliOutput)).toBe(1)
        await next.stop()
      })

      it('should dedupe pending revalidation requests', async () => {
        await next.start()
        const revalidate = 5
        await next.patchFile(
          'app/test/page.tsx',
          outdent`
          async function getTime() {
            const res = await fetch("http://localhost:${next.appPort}/api/time", { next: { revalidate: ${revalidate} } })
            return res.text()
          }
          
          export default async function Home() {
            await getTime()
            await getTime()
            const time = await getTime()
          
            return <h1>{time}</h1>
          }`
        )

        await next.render('/test')

        expect(invocation(next.cliOutput)).toBe(1)

        // wait for the revalidation to finish
        await retry(async () => {
          await next.render('/test')
          expect(invocation(next.cliOutput)).toBe(2)
          await next.stop()
        }, 10_000)
      })

      it('dedupes requests with different trace headers', async () => {
        await next.start()
        await next.patchFile(
          'app/test/page.tsx',
          outdent`
          async function getTime(traceId: string) {
            const res = await fetch("http://localhost:${next.appPort}/api/time", {
              headers: {
                'traceparent': '00-\${traceId}-b7ad6b7169203331-01',
                'tracestate': 'vendor1=value1'
              }
            })
            return res.text()
          }
          
          export default async function Home() {
            await getTime('b7ad6b7169203331')
            await getTime('c7ad6b7169203332')
            const time = await getTime('d7ad6b7169203333')
          
            return <h1>{time}</h1>
          }`
        )

        await next.render('/test')

        expect(invocation(next.cliOutput)).toBe(1)
        await next.stop()
      })
    })
  } else {
    it('should skip other scenarios', () => {})
    return
  }
})
Quest for Codev2.0.0
/
SIGN IN