next.js/test/e2e/app-dir/fallback-shells/fallback-shells.test.ts
fallback-shells.test.ts431 lines16.5 KB
import { nextTestSetup } from 'e2e-utils'
import { assertNoConsoleErrors } from 'next-test-utils'

describe('fallback-shells', () => {
  const { next, isNextDev, isNextDeploy, isNextStart } = nextTestSetup({
    files: __dirname,
  })

  describe('without IO', () => {
    it('should start and not postpone the response', async () => {
      const { browser, response } =
        await next.browserWithResponse('/without-io/world')

      expect(await browser.elementById('slug').text()).toBe('Hello /world')
      const headers = response.headers()

      // If we didn't use the fallback shell, then we didn't postpone the
      // response, and therefore shouldn't have sent the postponed header.
      expect(headers['x-nextjs-postponed']).not.toBe('1')
    })
  })

  describe('with cached IO', () => {
    describe('with generateStaticParams', () => {
      describe('and the page wrapped in Suspense', () => {
        describe('and the params accessed in the cached page', () => {
          it('resumes a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/with-suspense/params-in-page/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).toBe('1')
            }
          })

          it('does not produce hydration errors when resuming a fallback shell containing a layout with unused params', async () => {
            const browser = await next.browser(
              '/with-cached-io/with-static-params/with-suspense/params-in-page/bar',
              { pushErrorAsConsoleLog: true }
            )

            // There should also be no hydration errors due to a buildtime date
            // being replaced by a new runtime date.
            await assertNoConsoleErrors(browser)
          })

          // TODO: To be implemented in NAR-136.
          it.skip('includes a cached layout with unused params in the fallback shell', async () => {
            const browser = await next.browser(
              '/with-cached-io/with-static-params/with-suspense/params-in-page/bar'
            )

            const layout = await browser.elementById('layout').text()

            // When prerendered, this should be restored from the RDC during the
            // resume of the fallback shell, so it should be "buildtime". If the
            // layout is unexpectedly a cache miss, then it will be "runtime".
            expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')
          })

          // TODO: Activate for deploy tests once background revalidation for
          // prerendered pages is not triggered anymore on the first visit.
          if (!isNextDeploy) {
            it('shares a cached parent layout between a prerendered route shell and the fallback shell', async () => {
              // `/foo` was prerendered
              const browser = await next.browser(
                '/with-cached-io/with-static-params/with-suspense/params-in-page/foo'
              )

              const layoutDateRouteShell = await browser
                .elementById('root-layout')
                .text()

              expect(layoutDateRouteShell).toInclude(
                isNextDev ? 'runtime' : 'buildtime'
              )

              await browser.loadPage(
                new URL(
                  // Use a unique slug so earlier tests don't upgrade this route.
                  `/with-cached-io/with-static-params/with-suspense/params-in-page/baz`,
                  next.url
                ).href
              )

              const layoutDateFallbackShell = await browser
                .elementById('root-layout')
                .text()

              expect(layoutDateRouteShell).toInclude(
                isNextDev ? 'runtime' : 'buildtime'
              )

              expect(layoutDateFallbackShell).toBe(layoutDateRouteShell)
            })

            // TODO: To be implemented in NAR-136.
            it.skip('shares a cached layout with unused params between a prerendered route shell and the fallback shell', async () => {
              // `/foo` was prerendered
              const browser = await next.browser(
                '/with-cached-io/with-static-params/with-suspense/params-in-page/foo'
              )

              const layoutDateRouteShell = await browser
                .elementById('layout')
                .text()

              expect(layoutDateRouteShell).toInclude(
                isNextDev ? 'runtime' : 'buildtime'
              )

              // `/bar` was not prerendered, and thus resumes the fallback shell.
              await browser.loadPage(
                new URL(
                  '/with-cached-io/with-static-params/with-suspense/params-in-page/bar',
                  next.url
                ).href
              )

              const layoutDateFallbackShell = await browser
                .elementById('layout')
                .text()

              expect(layoutDateRouteShell).toInclude(
                isNextDev ? 'runtime' : 'buildtime'
              )

              expect(layoutDateFallbackShell).toBe(layoutDateRouteShell)
            })
          }
        })

        describe('and the params accessed in cached non-page function', () => {
          it('resumes a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/with-suspense/params-not-in-page/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).toBe('1')
            }
          })
        })

        describe('and params.then/catch/finally passed to a cached function', () => {
          it('resumes a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/with-suspense/params-then-in-page/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).toBe('1')
            }
          })
        })

        describe('and the params transformed with an async function and then passed to a cached function', () => {
          it('resumes a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/with-suspense/params-transformed/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).toBe('1')
            }
          })
        })
      })

      describe('and the page not wrapped in Suspense', () => {
        describe('and the params accessed in the cached page', () => {
          it('does not resume a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/without-suspense/params-in-page/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude('runtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).not.toBe('1')
            }
          })

          // TODO: Re-enable as deploy test when (potential) infra issue is
          // resolved.
          if (!isNextDeploy) {
            it('does not render a fallback shell when using a params placeholder', async () => {
              // This should trigger a blocking prerender of the route shell.
              const { browser, response } = await next.browserWithResponse(
                '/with-cached-io/with-static-params/without-suspense/params-in-page/[slug]'
              )

              expect(response.status()).toBe(200)

              // This should render the encoded param in the route shell, and not
              // interpret the param as a fallback param, and subsequently try to
              // render the fallback shell instead, which would fail because of the
              // missing parent suspense boundary.
              const lastModified = await browser
                .elementById('last-modified')
                .text()
              expect(lastModified).toInclude('Page /%5Bslug%5D')
              expect(lastModified).toInclude('runtime')
            })
          }
        })

        describe('and the params accessed in a cached non-page function', () => {
          it('does not resume a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/without-suspense/params-not-in-page/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude('runtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).not.toBe('1')
            }
          })
        })

        describe('and params.then/catch/finally passed to a cached function', () => {
          it('does not resume a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/without-suspense/params-then-in-page/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude('runtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).not.toBe('1')
            }
          })
        })

        describe('and the params transformed with an async function and then passed to a cached function', () => {
          it('does not resume a postponed fallback shell', async () => {
            const { browser, response } = await next.browserWithResponse(
              '/with-cached-io/with-static-params/without-suspense/params-transformed/bar'
            )

            const lastModified = await browser
              .elementById('last-modified')
              .text()
            expect(lastModified).toInclude('Page /bar')
            expect(lastModified).toInclude('runtime')

            const layout = await browser.elementById('root-layout').text()
            expect(layout).toInclude('runtime')

            const headers = response.headers()

            if (isNextStart) {
              expect(headers['x-nextjs-postponed']).not.toBe('1')
            }
          })
        })
      })
    })

    describe('without generateStaticParams', () => {
      describe('and the params accessed in the cached page', () => {
        it('resumes a postponed fallback shell', async () => {
          const { browser, response } = await next.browserWithResponse(
            '/with-cached-io/without-static-params/params-in-page/foo'
          )

          const lastModified = await browser.elementById('last-modified').text()
          expect(lastModified).toInclude('Page /foo')
          expect(lastModified).toInclude('runtime')

          const layout = await browser.elementById('root-layout').text()
          expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

          const headers = response.headers()

          if (isNextStart) {
            expect(headers['x-nextjs-postponed']).toBe('1')
          }
        })

        // TODO: To be implemented in NAR-136.
        it.skip('does not produce hydration errors when resuming a fallback shell containing a layout with unused params', async () => {
          const browser = await next.browser(
            '/with-cached-io/without-static-params/params-in-page/bar',
            { pushErrorAsConsoleLog: true }
          )

          // There should also be no hydration errors due to a buildtime date
          // being replaced by a new runtime date.
          await assertNoConsoleErrors(browser)
        })

        // TODO: To be implemented in NAR-136.
        it.skip('includes a cached layout with unused params in the fallback shell', async () => {
          const browser = await next.browser(
            '/with-cached-io/without-static-params/params-in-page/bar'
          )

          const layout = await browser.elementById('layout').text()

          // When prerendered, this should be restored from the RDC during the
          // resume of the fallback shell, so it should be "buildtime". If the
          // layout is unexpectedly a cache miss, then it will be "runtime".
          expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')
        })
      })

      describe('and the params accessed in cached non-page function', () => {
        it('resumes a postponed fallback shell', async () => {
          const { browser, response } = await next.browserWithResponse(
            '/with-cached-io/without-static-params/params-not-in-page/foo'
          )

          const lastModified = await browser.elementById('last-modified').text()
          expect(lastModified).toInclude('Page /foo')
          expect(lastModified).toInclude('runtime')

          const layout = await browser.elementById('root-layout').text()
          expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

          const headers = response.headers()

          if (isNextStart) {
            expect(headers['x-nextjs-postponed']).toBe('1')
          }
        })
      })

      describe('and params.then/catch/finally passed to a cached function', () => {
        it('resumes a postponed fallback shell', async () => {
          const { browser, response } = await next.browserWithResponse(
            '/with-cached-io/without-static-params/params-then-in-page/foo'
          )

          const lastModified = await browser.elementById('last-modified').text()
          expect(lastModified).toInclude('Page /foo')
          expect(lastModified).toInclude('runtime')

          const layout = await browser.elementById('root-layout').text()
          expect(layout).toInclude(isNextDev ? 'runtime' : 'buildtime')

          const headers = response.headers()

          if (isNextStart) {
            expect(headers['x-nextjs-postponed']).toBe('1')
          }
        })
      })
    })
  })

  if (isNextStart) {
    it('should not log a HANGING_PROMISE_REJECTION error', async () => {
      expect(next.cliOutput).not.toContain('HANGING_PROMISE_REJECTION')
    })
  }
})
Quest for Codev2.0.0
/
SIGN IN