mirror of
https://github.com/hanzoai/next.js.git
synced 2026-09-24 17:00:51 +00:00
In https://github.com/vercel/next.js/pull/69282, we landed support for Partial Fallback Prerendering. The intention behind this feature was to be able to upgrade from a partial fallback (essentially the UI produced when `await params` is dynamic, ie the param was not part of `generateStaticParams` at build time) to a route shell (when the param values are filled in, and thus we can prerender more of the page). It was reverted in https://github.com/vercel/next.js/pull/79258 because we didn't add first-class support to this in the build output API and the necessary infrastructure changes to perform similar behavior that this PR does to trigger a background revalidation with params filled in. This re-lands the feature in Next.js now that upstream support landed (https://github.com/vercel/vercel/pull/14703, https://github.com/vercel/vercel/pull/15338, and Vercel infra changes which are rolling out)
431 lines
16 KiB
TypeScript
431 lines
16 KiB
TypeScript
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')
|
|
})
|
|
}
|
|
})
|