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')
})
}
})