From ad1b754fd9edf9f13b8bfb7659a63b1995ca6eef Mon Sep 17 00:00:00 2001 From: magnus Date: Fri, 21 Mar 2025 16:24:43 +0100 Subject: [PATCH 1/6] add e2e for dynamicparams in app-router --- .../isr/dynamic-params-false/[id]/page.tsx | 43 +++++++++++++++++ .../app/isr/dynamic-params-true/[id]/page.tsx | 46 +++++++++++++++++++ .../tests-e2e/tests/appRouter/isr.test.ts | 44 ++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx create mode 100644 examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx diff --git a/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx b/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx new file mode 100644 index 000000000..9e0f00c60 --- /dev/null +++ b/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx @@ -0,0 +1,43 @@ +export const dynamicParams = false; // or false, to 404 on unknown paths + +interface Post { + id: string; + title: string; + content: string; +} + +const POSTS = Array.from({ length: 20 }, (_, i) => ({ + id: String(i + 1), + title: `Post ${i + 1}`, + content: `This is post ${i + 1}`, +})); + +async function fakeGetPostsFetch() { + return POSTS.slice(0, 10); +} + +async function fakeGetPostFetch(id: string) { + return POSTS.find((post) => post.id === id); +} + +export async function generateStaticParams() { + const fakePosts = await fakeGetPostsFetch(); + return fakePosts.map((post) => ({ + id: post.id, + })); +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const post = (await fakeGetPostFetch(id)) as Post; + return ( +
+

{post.title}

+

{post.content}

+
+ ); +} diff --git a/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx b/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx new file mode 100644 index 000000000..4ba10ce86 --- /dev/null +++ b/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx @@ -0,0 +1,46 @@ +// We'll prerender only the params from `generateStaticParams` at build time. +// If a request comes in for a path that hasn't been generated, +// Next.js will server-render the page on-demand. +export const dynamicParams = true; // or false, to 404 on unknown paths + +interface Post { + id: string; + title: string; + content: string; +} + +const POSTS = Array.from({ length: 20 }, (_, i) => ({ + id: String(i + 1), + title: `Post ${i + 1}`, + content: `This is post ${i + 1}`, +})); + +async function fakeGetPostsFetch() { + return POSTS.slice(0, 10); +} + +async function fakeGetPostFetch(id: string) { + return POSTS.find((post) => post.id === id); +} + +export async function generateStaticParams() { + const fakePosts = await fakeGetPostsFetch(); + return fakePosts.map((post) => ({ + id: post.id, + })); +} + +export default async function Page({ + params, +}: { + params: Promise<{ id: string }>; +}) { + const { id } = await params; + const post = (await fakeGetPostFetch(id)) as Post; + return ( +
+

{post.title}

+

{post.content}

+
+ ); +} diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index 4f6ae78ef..09d390878 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -93,3 +93,47 @@ test("Incremental Static Regeneration with data cache", async ({ page }) => { expect(originalCachedDate).toEqual(finalCachedDate); expect(originalFetchedDate).toEqual(finalFetchedDate); }); + +test("dynamicParams set to true", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-true/1"); + expect(res?.status()).toEqual(200); + expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT"); + const title = await page.getByTestId("title").textContent(); + const content = await page.getByTestId("content").textContent(); + expect(title).toEqual("Post 1"); + expect(content).toEqual("This is post 1"); + + // should SSR for a path that has not been generated + const res2 = await page.goto("/isr/dynamic-params-true/11"); + expect(res2?.headers()["x-nextjs-cache"]).toEqual("MISS"); + const title2 = await page.getByTestId("title").textContent(); + const content2 = await page.getByTestId("content").textContent(); + expect(title2).toEqual("Post 11"); + expect(content2).toEqual("This is post 11"); + + // should 500 for a non-existing path + const res3 = await page.goto("/isr/dynamic-params-true/21"); + expect(res3?.status()).toEqual(500); + expect(res3?.headers()["cache-control"]).toBe( + "private, no-cache, no-store, max-age=0, must-revalidate", + ); +}); + +test("dynamicParams set to false", async ({ page }) => { + // should return 200 and x-nextjs-cache HIT for an existing path + const res = await page.goto("/isr/dynamic-params-false/1"); + expect(res?.status()).toEqual(200); + expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT"); + const title = await page.getByTestId("title").textContent(); + const content = await page.getByTestId("content").textContent(); + expect(title).toEqual("Post 1"); + expect(content).toEqual("This is post 1"); + + // should return 404 for a non-existing path + const res2 = await page.goto("/isr/dynamic-params-false/11"); + expect(res2?.status()).toEqual(404); + expect(res2?.headers()["cache-control"]).toBe( + "private, no-cache, no-store, max-age=0, must-revalidate", + ); + await expect(await page.getByText("404")).toBeAttached(); +}); From 97e1106b77cf3dcb332eaad47b5f0ced95c57b1f Mon Sep 17 00:00:00 2001 From: magnus Date: Fri, 21 Mar 2025 16:29:57 +0100 Subject: [PATCH 2/6] fix comment --- examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx b/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx index 9e0f00c60..7a93286d4 100644 --- a/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx +++ b/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx @@ -1,4 +1,4 @@ -export const dynamicParams = false; // or false, to 404 on unknown paths +export const dynamicParams = false; // or true, to make it try SSR unknown paths interface Post { id: string; From 4fd7f0bd1ff9a6f70a95c74589b583c39b1a3ac4 Mon Sep 17 00:00:00 2001 From: magnus Date: Mon, 24 Mar 2025 17:18:56 +0100 Subject: [PATCH 3/6] review --- .../isr/dynamic-params-false/[id]/page.tsx | 13 +-- .../app/isr/dynamic-params-true/[id]/page.tsx | 17 ++-- .../tests-e2e/tests/appRouter/isr.test.ts | 95 +++++++++++-------- 3 files changed, 69 insertions(+), 56 deletions(-) diff --git a/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx b/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx index 7a93286d4..4b9701730 100644 --- a/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx +++ b/examples/app-router/app/isr/dynamic-params-false/[id]/page.tsx @@ -1,11 +1,6 @@ +// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams export const dynamicParams = false; // or true, to make it try SSR unknown paths -interface Post { - id: string; - title: string; - content: string; -} - const POSTS = Array.from({ length: 20 }, (_, i) => ({ id: String(i + 1), title: `Post ${i + 1}`, @@ -33,11 +28,11 @@ export default async function Page({ params: Promise<{ id: string }>; }) { const { id } = await params; - const post = (await fakeGetPostFetch(id)) as Post; + const post = await fakeGetPostFetch(id); return (
-

{post.title}

-

{post.content}

+

{post?.title}

+

{post?.content}

); } diff --git a/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx b/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx index 4ba10ce86..8134d0bff 100644 --- a/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx +++ b/examples/app-router/app/isr/dynamic-params-true/[id]/page.tsx @@ -1,14 +1,11 @@ +import { notFound } from "next/navigation"; + // We'll prerender only the params from `generateStaticParams` at build time. // If a request comes in for a path that hasn't been generated, // Next.js will server-render the page on-demand. +// https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams export const dynamicParams = true; // or false, to 404 on unknown paths -interface Post { - id: string; - title: string; - content: string; -} - const POSTS = Array.from({ length: 20 }, (_, i) => ({ id: String(i + 1), title: `Post ${i + 1}`, @@ -36,7 +33,13 @@ export default async function Page({ params: Promise<{ id: string }>; }) { const { id } = await params; - const post = (await fakeGetPostFetch(id)) as Post; + const post = await fakeGetPostFetch(id); + if (Number(id) === 1337) { + throw new Error("This is an error!"); + } + if (!post) { + notFound(); + } return (

{post.title}

diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index 09d390878..2a143580a 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -94,46 +94,61 @@ test("Incremental Static Regeneration with data cache", async ({ page }) => { expect(originalFetchedDate).toEqual(finalFetchedDate); }); -test("dynamicParams set to true", async ({ page }) => { - const res = await page.goto("/isr/dynamic-params-true/1"); - expect(res?.status()).toEqual(200); - expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT"); - const title = await page.getByTestId("title").textContent(); - const content = await page.getByTestId("content").textContent(); - expect(title).toEqual("Post 1"); - expect(content).toEqual("This is post 1"); - - // should SSR for a path that has not been generated - const res2 = await page.goto("/isr/dynamic-params-true/11"); - expect(res2?.headers()["x-nextjs-cache"]).toEqual("MISS"); - const title2 = await page.getByTestId("title").textContent(); - const content2 = await page.getByTestId("content").textContent(); - expect(title2).toEqual("Post 11"); - expect(content2).toEqual("This is post 11"); - - // should 500 for a non-existing path - const res3 = await page.goto("/isr/dynamic-params-true/21"); - expect(res3?.status()).toEqual(500); - expect(res3?.headers()["cache-control"]).toBe( - "private, no-cache, no-store, max-age=0, must-revalidate", - ); +test.describe("dynamicParams set to true", () => { + test("should be HIT on a path that was prebuilt", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-true/1"); + expect(res?.status()).toEqual(200); + expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT"); + const title = await page.getByTestId("title").textContent(); + const content = await page.getByTestId("content").textContent(); + expect(title).toEqual("Post 1"); + expect(content).toEqual("This is post 1"); + }); + + test("should SSR on a path that was not prebuilt", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-true/11"); + expect(res?.headers()["x-nextjs-cache"]).toEqual("MISS"); + const title = await page.getByTestId("title").textContent(); + const content = await page.getByTestId("content").textContent(); + expect(title).toEqual("Post 11"); + expect(content).toEqual("This is post 11"); + }); + + test("should 404 for a path that is not found", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-false/11"); + expect(res?.status()).toEqual(404); + expect(res?.headers()["cache-control"]).toBe( + "private, no-cache, no-store, max-age=0, must-revalidate", + ); + await expect(page.getByText("404")).toBeAttached(); + }); + + test("should 500 for a path that throws an error", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-true/1337"); + expect(res?.status()).toEqual(500); + expect(res?.headers()["cache-control"]).toBe( + "private, no-cache, no-store, max-age=0, must-revalidate", + ); + }); }); -test("dynamicParams set to false", async ({ page }) => { - // should return 200 and x-nextjs-cache HIT for an existing path - const res = await page.goto("/isr/dynamic-params-false/1"); - expect(res?.status()).toEqual(200); - expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT"); - const title = await page.getByTestId("title").textContent(); - const content = await page.getByTestId("content").textContent(); - expect(title).toEqual("Post 1"); - expect(content).toEqual("This is post 1"); - - // should return 404 for a non-existing path - const res2 = await page.goto("/isr/dynamic-params-false/11"); - expect(res2?.status()).toEqual(404); - expect(res2?.headers()["cache-control"]).toBe( - "private, no-cache, no-store, max-age=0, must-revalidate", - ); - await expect(await page.getByText("404")).toBeAttached(); +test.describe("dynamicParams set to false", () => { + test("should be HIT on a path that was prebuilt", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-false/1"); + expect(res?.status()).toEqual(200); + expect(res?.headers()["x-nextjs-cache"]).toEqual("HIT"); + const title = await page.getByTestId("title").textContent(); + const content = await page.getByTestId("content").textContent(); + expect(title).toEqual("Post 1"); + expect(content).toEqual("This is post 1"); + }); + + test("should 404 for a path that is not found", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-false/11"); + expect(res?.status()).toEqual(404); + expect(res?.headers()["cache-control"]).toBe( + "private, no-cache, no-store, max-age=0, must-revalidate", + ); + await expect(page.getByText("404")).toBeAttached(); + }); }); From f073b69da7a821293ca4b4739d29f2c22ec3bb02 Mon Sep 17 00:00:00 2001 From: magnus Date: Tue, 25 Mar 2025 21:09:40 +0100 Subject: [PATCH 4/6] review --- packages/tests-e2e/tests/appRouter/isr.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index 2a143580a..e774fa01a 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -114,8 +114,8 @@ test.describe("dynamicParams set to true", () => { expect(content).toEqual("This is post 11"); }); - test("should 404 for a path that is not found", async ({ page }) => { - const res = await page.goto("/isr/dynamic-params-false/11"); + test("should 404 when you call notFound", async ({ page }) => { + const res = await page.goto("/isr/dynamic-params-true/20"); expect(res?.status()).toEqual(404); expect(res?.headers()["cache-control"]).toBe( "private, no-cache, no-store, max-age=0, must-revalidate", From dbedd5f66ea8bd13b227fea61639bc84286d7101 Mon Sep 17 00:00:00 2001 From: magnus Date: Tue, 25 Mar 2025 21:12:57 +0100 Subject: [PATCH 5/6] comment --- packages/tests-e2e/tests/appRouter/isr.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index e774fa01a..1c9f4f4b7 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -105,6 +105,8 @@ test.describe("dynamicParams set to true", () => { expect(content).toEqual("This is post 1"); }); + // In `next start` this test would fail on subsequent requests because `x-nextjs-cache` would be `HIT` + // However, once deployed to AWS, Cloudfront will cache `MISS` test("should SSR on a path that was not prebuilt", async ({ page }) => { const res = await page.goto("/isr/dynamic-params-true/11"); expect(res?.headers()["x-nextjs-cache"]).toEqual("MISS"); From d3ecf901235cefd11661158181c901576ac3f240 Mon Sep 17 00:00:00 2001 From: magnus Date: Tue, 25 Mar 2025 21:22:43 +0100 Subject: [PATCH 6/6] fix id --- packages/tests-e2e/tests/appRouter/isr.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tests-e2e/tests/appRouter/isr.test.ts b/packages/tests-e2e/tests/appRouter/isr.test.ts index 1c9f4f4b7..db7a23435 100644 --- a/packages/tests-e2e/tests/appRouter/isr.test.ts +++ b/packages/tests-e2e/tests/appRouter/isr.test.ts @@ -117,7 +117,7 @@ test.describe("dynamicParams set to true", () => { }); test("should 404 when you call notFound", async ({ page }) => { - const res = await page.goto("/isr/dynamic-params-true/20"); + const res = await page.goto("/isr/dynamic-params-true/21"); expect(res?.status()).toEqual(404); expect(res?.headers()["cache-control"]).toBe( "private, no-cache, no-store, max-age=0, must-revalidate",