Files
cloudflare__vinext/tests/hybrid-client-route-owner.test.ts
James Anderson f1efcc1e60 fix(cloudflare): harden canonical RSC warmup end to end (#3040)
* fix(cloudflare): validate reusable warm responses

* fix(cloudflare): retry staged warmup failures safely

* fix(cloudflare): extend staged propagation retries

* fix(build): filter Pages-owned RSC warm paths

* fix(cloudflare): keep CDN warmup correct with custom adapters and i18n

* fix(cloudflare): reject unusable CDN warm responses

* fix(cloudflare): keep CDN warmup bound to the deployed build

* fix(router): reuse warmed RSC on locale-prefixed hybrid routes

* fix(cloudflare): verify exact build and variant during CDN warmup

* fix(router): keep warmed RSC ownership rewrite-safe

* test(cloudflare): prove browser reuse of warmed RSC variants

* test(cloudflare): bind RSC warm proof to each deploy attempt

* fix(rsc): keep deferred loading shells contextual

* fix(build): keep CDN warm discovery route-correct

* fix(cache): preserve valid CDN policy during RSC warmup

* fix(cloudflare): warm the deployed Worker route host

* fix(cloudflare): preserve staged warmup guarantees

* fix(build): discover localized Pages warmup paths

* fix(cloudflare): wait for staged Worker before CDN warmup

* fix(cloudflare): preserve strict warmup deploy invariants

* fix(build): fail closed during CDN warm discovery
2026-08-24 23:03:14 +01:00

449 lines
15 KiB
TypeScript

/**
* Direct unit tests for the client-side `resolveHybridClientRouteOwner`.
*
* Mirrors the server-side `pagesRouteHasPriorityOverAppRoute` tests and
* adds the manifest-mocking plumbing needed to drive the client resolver
* with synthetic `__VINEXT_LINK_PREFETCH_ROUTES__` and
* `__VINEXT_PAGES_LINK_PREFETCH_ROUTES__` arrays.
*
* The hard guarantee these tests assert: the client and the server reach
* the same owner for the same (pages pattern, app pattern) pair. If a
* test here ever diverges from `tests/hybrid-route-priority.test.ts`,
* the hybrid invariant is broken and the next browser hard-navigation
* will go to the wrong router.
*
* The trie matcher expects Next.js-segment-encoded patternParts: static
* segments are plain strings, dynamic segments start with `:`, catch-alls
* end with `+`, and optional catch-alls end with `*`. See
* `routing/route-trie.ts`.
*/
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
import type {
VinextLinkPrefetchRoute,
VinextPagesLinkPrefetchRoute,
} from "../packages/vinext/src/client/vinext-next-data.js";
import {
toClientRewrites,
type ClientRewrites,
} from "../packages/vinext/src/client/client-rewrites.js";
import {
resolveHybridClientRewriteHref,
resolveHybridClientRouteOwner,
} from "../packages/vinext/src/shims/internal/hybrid-client-route-owner.js";
const APP_BASE = "http://localhost/";
type WindowState = {
app: VinextLinkPrefetchRoute[];
locales?: string[];
pages: VinextPagesLinkPrefetchRoute[];
rewrites?: ClientRewrites;
};
function installWindow({ app, locales, pages, rewrites }: WindowState): void {
(globalThis as any).window = {
location: { href: APP_BASE, origin: "http://localhost" },
__VINEXT_LINK_PREFETCH_ROUTES__: app,
__VINEXT_LOCALES__: locales,
__VINEXT_PAGES_LINK_PREFETCH_ROUTES__: pages,
__VINEXT_CLIENT_REWRITES__: rewrites,
};
}
function uninstallWindow(): void {
delete (globalThis as any).window;
}
let savedWindow: unknown;
beforeEach(() => {
savedWindow = (globalThis as any).window;
});
afterEach(() => {
if (savedWindow === undefined) {
uninstallWindow();
} else {
(globalThis as any).window = savedWindow;
}
});
const appRoute = (patternParts: string[], isDynamic = true): VinextLinkPrefetchRoute => ({
canPrefetchLoadingShell: !isDynamic,
isDynamic,
patternParts,
});
const pagesRoute = (patternParts: string[], isDynamic = true): VinextPagesLinkPrefetchRoute => ({
canPrefetchLoadingShell: false,
isDynamic,
patternParts,
});
const documentRoute = (patternParts: string[], isDynamic = true): VinextLinkPrefetchRoute => ({
canPrefetchLoadingShell: false,
documentOnly: true,
isDynamic,
patternParts,
});
describe("resolveHybridClientRouteOwner", () => {
it("returns null when neither router has a matching manifest", () => {
installWindow({ app: [], pages: [] });
expect(resolveHybridClientRouteOwner("/missing", "")).toBeNull();
});
it("returns null when neither router matched the URL", () => {
installWindow({
app: [appRoute(["a"])],
pages: [pagesRoute(["b"])],
});
expect(resolveHybridClientRouteOwner("/c", "")).toBeNull();
});
it("returns 'app' when only the App manifest matched", () => {
installWindow({
app: [appRoute(["a", ":slug"])],
pages: [pagesRoute(["b"])],
});
expect(resolveHybridClientRouteOwner("/a/foobar", "")).toBe("app");
});
it("returns 'pages' when only the Pages manifest matched", () => {
installWindow({
app: [appRoute(["a"])],
pages: [pagesRoute(["b", ":slug"])],
});
expect(resolveHybridClientRouteOwner("/b/foobar", "")).toBe("pages");
});
it("returns document ownership for App route handlers and Pages API routes", () => {
installWindow({
app: [documentRoute(["app-api"], false)],
pages: [{ ...pagesRoute(["api", ":slug"]), documentOnly: true }],
});
expect(resolveHybridClientRouteOwner("/app-api", "")).toBe("document");
expect(resolveHybridClientRouteOwner("/api/test", "")).toBe("document");
});
it("keeps locale-prefixed App pages distinct from unprefixed Pages API routes", () => {
// Next.js treats /fr/api/* as a localized page path, not a Pages API path:
// test/e2e/i18n-api-support/index.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/i18n-api-support/index.test.ts
installWindow({
app: [appRoute([":locale", "api", "status"])],
locales: ["en", "fr"],
pages: [{ ...pagesRoute(["api", "status"], false), documentOnly: true }],
});
expect(resolveHybridClientRouteOwner("/fr/api/status", "")).toBe("app");
expect(resolveHybridClientRouteOwner("/api/status", "")).toBe("document");
});
it("still locale-normalizes Pages page matching", () => {
installWindow({
app: [appRoute([":locale", "dashboard"])],
locales: ["en", "fr"],
pages: [pagesRoute(["about"], false)],
});
expect(resolveHybridClientRouteOwner("/fr/about", "")).toBe("pages");
});
it("applies document ownership only after choosing the most specific route", () => {
installWindow({
app: [appRoute(["api", "settings"], false)],
pages: [{ ...pagesRoute(["api", ":slug"]), documentOnly: true }],
});
expect(resolveHybridClientRouteOwner("/api/settings", "")).toBe("app");
installWindow({
app: [documentRoute(["docs", ":slug"])],
pages: [pagesRoute(["docs", "settings"], false)],
});
expect(resolveHybridClientRouteOwner("/docs/settings", "")).toBe("pages");
});
it.each(["beforeFiles", "afterFiles", "fallback"] as const)(
"resolves %s rewrites before choosing the route owner",
(rewritePhase) => {
installWindow({
app: [appRoute(["app-destination"], false)],
pages: [pagesRoute(["pages-destination"], false)],
rewrites: {
beforeFiles:
rewritePhase === "beforeFiles"
? [{ source: "/source", destination: "/app-destination" }]
: [],
afterFiles:
rewritePhase === "afterFiles"
? [{ source: "/source", destination: "/app-destination" }]
: [],
fallback:
rewritePhase === "fallback"
? [{ source: "/source", destination: "/app-destination" }]
: [],
},
});
expect(resolveHybridClientRouteOwner("/source", "")).toBe("app");
},
);
it("matches locale-aware rewrites against the raw App pathname", () => {
installWindow({
app: [appRoute([":locale", "app-destination"])],
locales: ["en", "fr"],
pages: [],
rewrites: {
afterFiles: [],
beforeFiles: [
{
source: "/:nextInternalLocale(en|fr)/source",
destination: "/:nextInternalLocale/app-destination",
},
],
fallback: [],
},
});
expect(resolveHybridClientRouteOwner("/fr/source", "")).toBe("app");
});
it.each(["beforeFiles", "afterFiles", "fallback"] as const)(
"returns the %s rewrite destination href",
(rewritePhase) => {
installWindow({
app: [appRoute(["app-destination"], false)],
pages: [],
rewrites: {
beforeFiles:
rewritePhase === "beforeFiles"
? [{ source: "/source", destination: "/app-destination" }]
: [],
afterFiles:
rewritePhase === "afterFiles"
? [{ source: "/source", destination: "/app-destination" }]
: [],
fallback:
rewritePhase === "fallback"
? [{ source: "/source", destination: "/app-destination" }]
: [],
},
});
expect(resolveHybridClientRewriteHref("/source", "")).toBe("/app-destination");
},
);
it("evaluates conditional client rewrites against browser-visible context", () => {
installWindow({
app: [appRoute(["app-destination"], false)],
pages: [],
rewrites: {
afterFiles: [],
beforeFiles: [
{
source: "/source",
destination: "/app-destination",
has: [{ type: "query", key: "preview", value: "1" }],
},
],
fallback: [],
},
});
expect(resolveHybridClientRouteOwner("/source", "")).toBeNull();
expect(resolveHybridClientRouteOwner("/source?preview=1", "")).toBe("app");
});
it("hands rewrites with browser-invisible conditions back to the document request", () => {
installWindow({
app: [appRoute(["app-destination"], false)],
pages: [],
rewrites: toClientRewrites({
afterFiles: [],
beforeFiles: [
{
source: "/cookie-source",
destination: "/app-destination",
has: [{ type: "cookie", key: "internal-access", value: "cookie-secret" }],
},
{
source: "/header-source",
destination: "/app-destination",
has: [{ type: "header", key: "x-origin-auth", value: "header-secret" }],
},
],
fallback: [],
}),
});
expect(resolveHybridClientRewriteHref("/cookie-source", "")).toBeNull();
expect(resolveHybridClientRouteOwner("/cookie-source", "")).toBe("document");
expect(resolveHybridClientRewriteHref("/header-source", "")).toBeNull();
expect(resolveHybridClientRouteOwner("/header-source", "")).toBe("document");
});
it("uses browser-authoritative conditions as a server handoff prefilter", () => {
installWindow({
app: [appRoute(["app-destination"], false)],
pages: [],
rewrites: toClientRewrites({
afterFiles: [],
beforeFiles: [
{
source: "/source",
destination: "/app-destination",
has: [
{ type: "query", key: "preview", value: "1" },
{ type: "cookie", key: "internal-access", value: "cookie-secret" },
],
},
],
fallback: [],
}),
});
expect(resolveHybridClientRouteOwner("/source", "")).toBeNull();
expect(resolveHybridClientRouteOwner("/source?preview=1", "")).toBe("document");
});
it("applies every beforeFiles rewrite before choosing ownership", () => {
installWindow({
app: [],
pages: [pagesRoute(["pages-destination"], false)],
rewrites: {
afterFiles: [],
beforeFiles: [
{ source: "/source", destination: "/intermediate?first=1" },
{ source: "/intermediate", destination: "/pages-destination?second=2" },
],
fallback: [],
},
});
expect(resolveHybridClientRouteOwner("/source?original=1", "")).toBe("pages");
});
it.each(["afterFiles", "fallback"] as const)(
"continues through unmatched %s rewrite destinations",
(rewritePhase) => {
installWindow({
app: [appRoute(["app-destination"], false)],
pages: [],
rewrites: {
afterFiles:
rewritePhase === "afterFiles"
? [
{ source: "/source", destination: "/intermediate" },
{ source: "/intermediate", destination: "/app-destination" },
]
: [],
beforeFiles: [],
fallback:
rewritePhase === "fallback"
? [
{ source: "/source", destination: "/intermediate" },
{ source: "/intermediate", destination: "/app-destination" },
]
: [],
},
});
expect(resolveHybridClientRouteOwner("/source", "")).toBe("app");
},
);
it("lets a more specific Pages dynamic route beat an App root catch-all", () => {
// Mirrors the server test of the same name. /pages-dir/:dynamic
// (score 51) beats /:path+ (score 1000).
installWindow({
app: [appRoute([":path+"])],
pages: [pagesRoute(["pages-dir", ":dynamic"])],
});
expect(resolveHybridClientRouteOwner("/pages-dir/foobar", "")).toBe("pages");
});
it("lets an App static route own the request when Pages only has a catch-all", () => {
installWindow({
app: [appRoute(["dashboard"], false)],
pages: [pagesRoute([":path+"])],
});
expect(resolveHybridClientRouteOwner("/dashboard", "")).toBe("app");
});
it("lets a static Pages route win over a dynamic App catch-all", () => {
// E.g. Pages has a literal `/about` page, App only has a catch-all.
// The literal Pages hit must own the request even though the App
// catch-all matches the same URL.
installWindow({
app: [appRoute([":path+"])],
pages: [pagesRoute(["about"], false)],
});
expect(resolveHybridClientRouteOwner("/about", "")).toBe("pages");
});
it("rejects an identical static App and Pages route", () => {
installWindow({
app: [appRoute([], false)],
pages: [pagesRoute([], false)],
});
expect(() => resolveHybridClientRouteOwner("/", "")).toThrow("Conflicting app and page routes");
});
it("retains Pages provider order after merged route validation", () => {
installWindow({
app: [appRoute([":slug"])],
pages: [pagesRoute([":id"])],
});
expect(resolveHybridClientRouteOwner("/anything", "")).toBe("pages");
});
it("lets a static-prefix Pages catch-all beat a bare App catch-all", () => {
// `/_sites/:slug*` (score 1951 with the static-prefix reduction)
// must beat `/:slug*` (score 2000). The previous hand-copied
// comparator missed the static-prefix reduction and returned 'app'
// for this case, splitting client / server ownership.
installWindow({
app: [appRoute([":slug*"])],
pages: [pagesRoute(["_sites", ":slug*"])],
});
expect(resolveHybridClientRouteOwner("/_sites/anything/here", "")).toBe("pages");
});
it("lets a static-prefix Pages dynamic beat a bare App dynamic", () => {
installWindow({
app: [appRoute([":subdomain"])],
pages: [pagesRoute(["_sites", ":subdomain"])],
});
expect(resolveHybridClientRouteOwner("/_sites/foo", "")).toBe("pages");
});
it("prioritizes an earlier static App segment over a later static Pages segment", () => {
installWindow({
app: [appRoute(["account", ":tab"])],
pages: [pagesRoute([":section", "details"])],
});
expect(resolveHybridClientRouteOwner("/account/details", "")).toBe("app");
});
it("keeps an exact App dynamic route ahead of a Pages optional catch-all", () => {
installWindow({
app: [appRoute([":section"])],
pages: [pagesRoute([":section", ":rest*"])],
});
expect(resolveHybridClientRouteOwner("/foo", "")).toBe("app");
});
it("ignores the basePath prefix when matching", () => {
installWindow({
app: [appRoute(["a"])],
pages: [pagesRoute(["pages-dir", ":dynamic"])],
});
// The client strips the basePath before consulting the manifest,
// matching the server's normalisation.
expect(resolveHybridClientRouteOwner("/base/pages-dir/foobar", "/base")).toBe("pages");
expect(resolveHybridClientRouteOwner("/base/a", "/base")).toBe("app");
});
});