Skip to content

Commit 1b452eb

Browse files
committed
test(e2e): Add Next.js cache component nesting scenarios
1 parent d0b4cca commit 1b452eb

22 files changed

Lines changed: 743 additions & 0 deletions

File tree

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { ReactNode } from 'react';
2+
import { cacheLife } from 'next/cache';
3+
4+
// The awaited param puts the request id into the cache key, so a fresh id is a guaranteed miss
5+
// and the entry cannot come from a build-time fill. `children` passes through as an uncached hole.
6+
export default async function CachedMidLayout({
7+
children,
8+
params,
9+
}: {
10+
children: ReactNode;
11+
params: Promise<{ id: string }>;
12+
}) {
13+
'use cache';
14+
cacheLife('hours');
15+
const { id } = await params;
16+
await new Promise(resolve => setTimeout(resolve, 100));
17+
return (
18+
<div data-testid="cached-mid-layout">
19+
<p id="cached-layout-stamp">
20+
{id}:{Date.now()}
21+
</p>
22+
{children}
23+
</div>
24+
);
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { headers } from 'next/headers';
2+
3+
export default async function Page() {
4+
await headers();
5+
return <p id="dynamic-leaf">Dynamic leaf: {Date.now()}</p>;
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { cacheLife } from 'next/cache';
2+
3+
async function CachedLeaf({ id }: { id: string }) {
4+
'use cache';
5+
cacheLife('hours');
6+
await new Promise(resolve => setTimeout(resolve, 100));
7+
return (
8+
<p id="cached-leaf">
9+
{id}:{Date.now()}
10+
</p>
11+
);
12+
}
13+
14+
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
15+
const { id } = await params;
16+
return <CachedLeaf id={id} />;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { ReactNode } from 'react';
2+
import { headers } from 'next/headers';
3+
4+
export default async function DynamicLayout({ children }: { children: ReactNode }) {
5+
await headers();
6+
return <div data-testid="dynamic-layout">{children}</div>;
7+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { Suspense, type ReactNode } from 'react';
2+
3+
// The Suspense boundary lets the nested layouts/pages below use request APIs (`headers()`,
4+
// runtime params) without tripping the Cache Components prerender guards.
5+
export default function Layout({ children }: { children: ReactNode }) {
6+
return (
7+
<div data-testid="cached-nesting-group">
8+
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
9+
</div>
10+
);
11+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ReactNode } from 'react';
2+
import { cacheLife } from 'next/cache';
3+
4+
// Hard-expires after 2s while the page's cached component lives for hours, so one request can
5+
// refill the layout while hitting the component: two cached levels with different origin traces.
6+
export default async function ShortLivedLayout({
7+
children,
8+
params,
9+
}: {
10+
children: ReactNode;
11+
params: Promise<{ id: string }>;
12+
}) {
13+
'use cache';
14+
cacheLife({ revalidate: 1, expire: 2 });
15+
const { id } = await params;
16+
return (
17+
<div data-testid="short-lived-layout">
18+
<p id="layout-stamp">
19+
{id}:{Date.now()}
20+
</p>
21+
{children}
22+
</div>
23+
);
24+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { headers } from 'next/headers';
2+
import { cacheLife } from 'next/cache';
3+
4+
async function LongLivedComponent({ id }: { id: string }) {
5+
'use cache';
6+
cacheLife('hours');
7+
await new Promise(resolve => setTimeout(resolve, 100));
8+
return (
9+
<p id="component-stamp">
10+
{id}:{Date.now()}
11+
</p>
12+
);
13+
}
14+
15+
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
16+
const { id } = await params;
17+
await headers();
18+
return <LongLivedComponent id={id} />;
19+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { headers } from 'next/headers';
2+
3+
export default async function Page() {
4+
await headers();
5+
return <p id="route-a">Route a: {Date.now()}</p>;
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { headers } from 'next/headers';
2+
3+
export default async function Page() {
4+
await headers();
5+
return <p id="route-b">Route b: {Date.now()}</p>;
6+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import type { ReactNode } from 'react';
2+
import { cacheLife } from 'next/cache';
3+
4+
// One cache entry (keyed by id) shared by the sibling routes `a` and `b` below.
5+
export default async function SharedLayout({
6+
children,
7+
params,
8+
}: {
9+
children: ReactNode;
10+
params: Promise<{ id: string }>;
11+
}) {
12+
'use cache';
13+
cacheLife('hours');
14+
const { id } = await params;
15+
await new Promise(resolve => setTimeout(resolve, 100));
16+
return (
17+
<div data-testid="shared-layout">
18+
<p id="shared-layout-stamp">
19+
{id}:{Date.now()}
20+
</p>
21+
{children}
22+
</div>
23+
);
24+
}

0 commit comments

Comments
 (0)