next.js/test/development/app-dir/server-components-hmr-cache/server-components-hmr-cache.test.ts
server-components-hmr-cache.test.ts238 lines8.6 KB
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'

const isCacheComponentsEnabled = process.env.__NEXT_CACHE_COMPONENTS === 'true'

describe('server-components-hmr-cache', () => {
  const { next } = nextTestSetup({ files: __dirname, patchFileDelay: 1000 })
  const loggedAfterValueRegexp = /After: (\d\.\d+)/
  let cliOutputLength: number

  const getLoggedAfterValue = () => {
    const match = next.cliOutput
      .slice(cliOutputLength)
      .match(loggedAfterValueRegexp)

    if (!match) {
      throw new Error('No logs from after() found')
    }
    return match[1]
  }

  // Edge runtime is not supported with Cache Components.
  const runtimes = isCacheComponentsEnabled ? ['node'] : ['edge', 'node']

  beforeEach(() => {
    cliOutputLength = next.cliOutput.length
  })

  describe.each(runtimes)('%s runtime', (runtime) => {
    it('should use cached fetch calls for fast refresh requests', async () => {
      const browser = await next.browser(`/${runtime}`)
      const valueBeforePatch = await browser.elementById('value').text()

      await next.patchFile(
        'components/shared-page.tsx',
        (content) => content.replace('foo', 'bar'),
        async () => {
          await retry(async () => {
            const updatedContent = await browser.elementById('content').text()
            expect(updatedContent).toBe('bar')
            // TODO: remove custom duration in case we increase the default.
          }, 5000)

          const valueAfterPatch = await browser.elementById('value').text()
          expect(valueBeforePatch).toEqual(valueAfterPatch)
        }
      )
    })

    it('should not use cached fetch calls for intentional refresh requests', async () => {
      const browser = await next.browser(`/${runtime}`)
      const valueBeforeRefresh = await browser.elementById('value').text()
      await browser.elementByCss(`button`).click().waitForIdleNetwork()

      await retry(async () => {
        const valueAfterRefresh = await browser.elementById('value').text()
        expect(valueBeforeRefresh).not.toEqual(valueAfterRefresh)
        // TODO: remove custom duration in case we increase the default.
      }, 5000)
    })

    describe('in after()', () => {
      it('should use cached fetch calls for fast refresh requests', async () => {
        const browser = await next.browser(`/${runtime}`)
        const valueBeforePatch = await retry(() => getLoggedAfterValue())
        cliOutputLength = next.cliOutput.length

        await next.patchFile(
          'components/shared-page.tsx',
          (content) => content.replace('foo', 'bar'),
          async () => {
            await retry(async () => {
              const updatedContent = await browser.elementById('content').text()
              expect(updatedContent).toBe('bar')
              // TODO: remove custom duration in case we increase the default.
            }, 5000)

            const valueAfterPatch = await retry(() => getLoggedAfterValue())
            expect(valueBeforePatch).toEqual(valueAfterPatch)
          }
        )
      })

      it('should not use cached fetch calls for intentional refresh requests', async () => {
        const browser = await next.browser(`/${runtime}`)
        const valueBeforeRefresh = await retry(() => getLoggedAfterValue())
        cliOutputLength = next.cliOutput.length

        await browser.elementByCss(`button`).click().waitForIdleNetwork()

        await retry(async () => {
          const valueAfterRefresh = getLoggedAfterValue()
          expect(valueBeforeRefresh).not.toEqual(valueAfterRefresh)
          // TODO: remove custom duration in case we increase the default.
        }, 5000)
      })
    })

    describe('with cacheMaxMemorySize set to 0', () => {
      beforeAll(async () => {
        await next.patchFile('next.config.js', (content) =>
          content.replace('// cacheMaxMemorySize: 0,', 'cacheMaxMemorySize: 0,')
        )
      })

      afterAll(async () => {
        await next.patchFile('next.config.js', (content) =>
          content.replace('cacheMaxMemorySize: 0,', '// cacheMaxMemorySize: 0,')
        )
      })

      it('should not warn about "Single item size exceeds maxSize"', async () => {
        const initialOutputLength = next.cliOutput.length
        const browser = await next.browser(`/${runtime}`)
        await browser.elementById('value').text()

        await next.patchFile(
          'components/shared-page.tsx',
          (content) => content.replace('foo', 'bar'),
          async () => {
            await retry(async () => {
              const updatedContent = await browser.elementById('content').text()
              expect(updatedContent).toBe('bar')
            }, 5000)

            // Verify the warning does not appear
            const newOutput = next.cliOutput.slice(initialOutputLength)
            expect(newOutput).not.toContain('Single item size exceeds maxSize')
          }
        )
      })

      it('should still use cached fetch calls for fast refresh requests', async () => {
        const browser = await next.browser(`/${runtime}`)
        const valueBeforePatch = await browser.elementById('value').text()

        await next.patchFile(
          'components/shared-page.tsx',
          (content) => content.replace('foo', 'bar'),
          async () => {
            await retry(async () => {
              const updatedContent = await browser.elementById('content').text()
              expect(updatedContent).toBe('bar')
            }, 5000)

            // HMR cache should still work even with cacheMaxMemorySize: 0
            const valueAfterPatch = await browser.elementById('value').text()
            expect(valueBeforePatch).toEqual(valueAfterPatch)
          }
        )
      })
    })

    describe('with experimental.serverComponentsHmrCache disabled', () => {
      beforeAll(async () => {
        // Wait for server to be ready
        await next.fetch('/404')
        await next.patchFile('next.config.js', (content) =>
          content.replace(
            '// serverComponentsHmrCache: false,',
            'serverComponentsHmrCache: false,'
          )
        )
      })

      afterAll(async () => {
        await next.patchFile('next.config.js', (content) =>
          content.replace(
            'serverComponentsHmrCache: false,',
            '// serverComponentsHmrCache: false,'
          )
        )
      })

      it('should not use cached fetch calls for fast refresh requests', async () => {
        const browser = await next.browser(`/${runtime}`)
        const valueBeforePatch = await browser.elementById('value').text()

        await next.patchFile(
          'components/shared-page.tsx',
          (content) => content.replace('foo', 'bar'),
          async () => {
            await retry(async () => {
              const updatedContent = await browser.elementById('content').text()
              expect(updatedContent).toBe('bar')
              // TODO: remove custom duration in case we increase the default.
            }, 5000)

            const valueAfterPatch = await browser.elementById('value').text()
            expect(valueBeforePatch).not.toEqual(valueAfterPatch)
          }
        )
      })

      describe('in after()', () => {
        beforeEach(() => {
          cliOutputLength = next.cliOutput.length
        })

        it('should not use cached fetch calls for fast refresh requests', async () => {
          const browser = await next.browser(`/${runtime}`)
          const valueBeforePatch = await retry(() => getLoggedAfterValue())
          cliOutputLength = next.cliOutput.length

          await next.patchFile(
            'components/shared-page.tsx',
            (content) => content.replace('foo', 'bar'),
            async () => {
              await retry(async () => {
                const updatedContent = await browser
                  .elementById('content')
                  .text()
                expect(updatedContent).toBe('bar')
                // TODO: remove custom duration in case we increase the default.
              }, 5000)

              const valueAfterPatch = await retry(() => getLoggedAfterValue())
              expect(valueBeforePatch).not.toEqual(valueAfterPatch)
            }
          )
        })
      })
    })
  })

  it('should support reading from an infinite streaming fetch', async () => {
    const browser = await next.browser('/infinite-stream')
    const text = await browser.elementByCss('p').text()
    expect(text).toBe('data: chunk-1')

    // The fetch is aborted after reading the first chunk, which is intentional,
    // so we shouldn't warn about failing to cache it.
    expect(next.cliOutput.slice(cliOutputLength)).not.toContain(
      'Failed to set fetch cache'
    )
  })
})
Quest for Codev2.0.0
/
SIGN IN