Files
next.js/test/e2e/app-dir/fallback-shells/fallback-shells.test.ts
Arian Tron 61f56f997c
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled
first commit
2026-03-10 19:37:31 +03:30

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