Files
cloudflare__vinext/tests/app-ppr-fallback-shell.test.ts
T
Nathan Nguyen 4688fa9c16 fix(app-router): prerender cacheComponents root-param fallback shells (#1702)
* fix(app-router): prerender cacheComponents root-param fallback shells

PR 1 of 4: core model + build integration.
Model: app-ppr-fallback-shell, pregenerated-concrete-paths, prerender-manifest
Build: prerender/run-prerender fallback shell artifact generation
Tests: createAppPprFallbackShells, pregenerated-concrete-paths core

* test(prerender-route-params): add unit tests for fallbackParamNames and validation

Adds focused unit tests for the new prerender-route-params logic introduced
in this PR:

- encodePrerenderRouteParams round-trips fallbackParamNames and omits
  them when empty.
- prerenderRouteParamsPayloadMatchesRoute rejects payloads whose
  fallbackParamNames contain unknown params or duplicates.
- prerenderRouteParamsPayloadMatchesRoute returns false for valid
  fallback-shell matches (only exact matches are accepted today).

Also adds a comment in app-ppr-fallback-shell.ts noting that placeholder
brackets in shell pathnames are percent-encoded by new URL() at fetch
time, so the render path must supply params via the prerender-params
header rather than URL matching.

* docs(prerender): clarify BUILD_ID reuse and fallback-shell param-resolution seams

Address review feedback on PR #1702:
- Explain why run-prerender reuses the built BUILD_ID instead of
  re-resolving one (config.buildId feeds prerendered-output identity).
- Note that the fallback-shell prerender-params header path described in
  app-ppr-fallback-shell.ts is honored by the #1715 follow-up; this
  generation-only PR still resolves params from the URL placeholder.

---------

Co-authored-by: James <james@eli.cx>
2026-06-02 16:11:48 +01:00

130 lines
4.1 KiB
TypeScript

import { describe, expect, it } from "vite-plus/test";
import {
createAppPprFallbackShell,
createAppPprFallbackShells,
rewriteAppPprFallbackShellHtmlNavigation,
} from "../packages/vinext/src/server/app-ppr-fallback-shell.js";
describe("createAppPprFallbackShell", () => {
it("builds a cacheComponents fallback shell from known root params and missing child params", () => {
const shell = createAppPprFallbackShell(
{
params: ["locale", "slug"],
pattern: "/:locale/blog/:slug",
rootParamNames: ["locale"],
},
{ locale: "en", slug: "new-post" },
);
expect(shell).toEqual({
fallbackParamNames: ["slug"],
pathname: "/en/blog/[slug]",
params: { locale: "en", slug: "[slug]" },
});
});
it("preserves catch-all placeholder shape in the shell path and params", () => {
const shell = createAppPprFallbackShell(
{
params: ["locale", "slug"],
pattern: "/:locale/docs/:slug+",
rootParamNames: ["locale"],
},
{ locale: "fr", slug: ["guides", "intro"] },
);
expect(shell).toEqual({
fallbackParamNames: ["slug"],
pathname: "/fr/docs/[...slug]",
params: { locale: "fr", slug: ["[...slug]"] },
});
});
it("preserves optional catch-all placeholder shape in the shell path and params", () => {
const shell = createAppPprFallbackShell(
{
params: ["locale", "slug"],
pattern: "/:locale/docs/:slug*",
rootParamNames: ["locale"],
},
{ locale: "fr", slug: ["guides", "intro"] },
);
expect(shell).toEqual({
fallbackParamNames: ["slug"],
pathname: "/fr/docs/[[...slug]]",
params: { locale: "fr", slug: ["[[...slug]]"] },
});
});
it("orders nested fallback shells from most specific child prefix to root-only", () => {
const shells = createAppPprFallbackShells(
{
params: ["locale", "category", "slug"],
pattern: "/:locale/category/:category/post/:slug",
rootParamNames: ["locale"],
},
{ locale: "en", category: "news", slug: "launch" },
);
expect(shells).toEqual([
{
fallbackParamNames: ["slug"],
pathname: "/en/category/news/post/[slug]",
params: { locale: "en", category: "news", slug: "[slug]" },
},
{
fallbackParamNames: ["category", "slug"],
pathname: "/en/category/[category]/post/[slug]",
params: { locale: "en", category: "[category]", slug: "[slug]" },
},
]);
});
it("does not create a fallback shell without a known root-param boundary", () => {
expect(
createAppPprFallbackShell(
{
params: ["locale", "slug"],
pattern: "/:locale/blog/:slug",
rootParamNames: [],
},
{ locale: "en", slug: "new-post" },
),
).toBeNull();
});
it("does not create a fallback shell when the matched request lacks a root param", () => {
expect(
createAppPprFallbackShell(
{
params: ["locale", "slug"],
pattern: "/:locale/blog/:slug",
rootParamNames: ["locale"],
},
{ slug: "new-post" },
),
).toBeNull();
});
});
describe("rewriteAppPprFallbackShellHtmlNavigation", () => {
it("patches cached fallback-shell HTML with the actual request navigation metadata", () => {
const html = rewriteAppPprFallbackShellHtmlNavigation({
html: "<html><head><title>x</title></head><body>shell</body></html>",
params: { locale: "en", slug: "new-post" },
pathname: "/en/blog/new-post",
searchParams: new URLSearchParams([["preview", "1"]]),
});
expect(html).toContain('params:{"locale":"en","slug":"new-post"}');
expect(html).toContain('"pathname":"/en/blog/new-post"');
expect(html).toContain('"searchParams":[["preview","1"]]');
const paramsIndex = html.indexOf('params:{"locale":"en","slug":"new-post"}');
const headCloseIndex = html.indexOf("</head>");
expect(paramsIndex).toBeGreaterThanOrEqual(0);
expect(headCloseIndex).toBeGreaterThanOrEqual(0);
expect(paramsIndex).toBeLessThan(headCloseIndex);
});
});