Files
cloudflare__vinext/tests/navigation-runtime.test.ts
T
Nathan Nguyen 0d8d3e3c60 refactor(router): quarantine navigation runtime topology (#1322)
* refactor(router): quarantine navigation runtime topology

* fix(router): restore manifest-backed interception runtime

App Router browser navigations lost first-hop intercepted route context after snapshot topology stopped acting as fallback proof. That made /feed to /photos/:id request the direct page payload instead of the declared modal payload.

The violated invariant was that route topology must come from the manifest, while snapshots only carry runtime proof. Resolve first-hop interception from the manifest, move RSC bootstrap streams onto the symbol-backed runtime, teach prerender extraction that script shape, and harden the runtime contract around route manifest and RSC data.

Coverage includes runtime seam tests, prerender extraction tests, planner/browser navigation tests, package rebuild, and the app-router intercepted routes E2E slice.

* fix(router): narrow prerender RSC script parsing

The prerender extractor accepted any script starting with the symbol-runtime bootstrap expression, then parsed it as though it had the exact RSC chunk-push prefix. That was harmless at the current head, but it made future runtime scripts a foot-gun.

Only the exact runtime chunk prefix and exact done marker are now accepted. A focused extractor test covers non-chunk runtime scripts that share the bootstrap expression prefix.

* fix(router): address navigation runtime review

* fix(router): resolve navigation runtime review threads
2026-05-20 10:29:13 +01:00

163 lines
4.9 KiB
TypeScript

import { afterEach, describe, expect, it } from "vite-plus/test";
import {
NAVIGATION_RUNTIME_KEY,
getNavigationRuntime,
hasAppNavigationRuntime,
registerNavigationRuntimeBootstrap,
registerNavigationRuntimeFunctions,
subscribeNavigationRuntimeRscChunk,
type NavigationRuntime,
type NavigationRuntimeBootstrap,
type NavigationRuntimeFunctions,
type NavigationRuntimeRscBootstrap,
type NavigationRuntimeRscChunk,
} from "../packages/vinext/src/client/navigation-runtime.js";
const originalWindow = Reflect.get(globalThis, "window");
const hadWindow = Reflect.has(globalThis, "window");
afterEach(() => {
if (hadWindow) {
Reflect.set(globalThis, "window", originalWindow);
return;
}
Reflect.deleteProperty(globalThis, "window");
});
describe("navigation runtime contract", () => {
it("merges bootstrap data without clobbering independently registered RSC payloads", () => {
Reflect.set(globalThis, "window", {});
const chunk: NavigationRuntimeRscChunk = "flight";
const rscBootstrap: NavigationRuntimeRscBootstrap = {
params: { id: "123" },
rsc: [chunk],
};
const bootstrap: Partial<NavigationRuntimeBootstrap> = { rsc: rscBootstrap };
registerNavigationRuntimeBootstrap(bootstrap);
registerNavigationRuntimeBootstrap({ routeManifest: null });
expect(getNavigationRuntime()?.bootstrap.rsc?.params?.id).toBe("123");
expect(getNavigationRuntime()?.bootstrap.routeManifest).toBeNull();
});
it("creates the RSC bootstrap buffer when subscribing the first chunk", () => {
Reflect.set(globalThis, "window", {});
subscribeNavigationRuntimeRscChunk("chunk");
expect(getNavigationRuntime()?.bootstrap.rsc?.rsc).toEqual(["chunk"]);
});
it("reports app navigation availability from the registered navigate slot", () => {
Reflect.set(globalThis, "window", {});
expect(hasAppNavigationRuntime()).toBe(false);
registerNavigationRuntimeFunctions({
navigate: () => Promise.resolve(),
});
expect(hasAppNavigationRuntime()).toBe(true);
});
it("merges registered function slots without clobbering existing capabilities", () => {
Reflect.set(globalThis, "window", {});
const navigate = () => Promise.resolve();
const pingVisibleLinks = () => {};
registerNavigationRuntimeFunctions({ navigate });
registerNavigationRuntimeFunctions({ pingVisibleLinks });
expect(getNavigationRuntime()?.functions.navigate).toBe(navigate);
expect(getNavigationRuntime()?.functions.pingVisibleLinks).toBe(pingVisibleLinks);
});
it("keeps server-side runtime creation detached from the window contract", () => {
Reflect.deleteProperty(globalThis, "window");
const runtime = subscribeNavigationRuntimeRscChunk("server-chunk");
expect(runtime.bootstrap.rsc?.rsc).toEqual(["server-chunk"]);
expect(getNavigationRuntime()).toBeNull();
});
it("rejects runtime objects with non-function capability slots", () => {
const runtimeWindow = {};
const functions: NavigationRuntimeFunctions = {};
const runtime: NavigationRuntime = {
bootstrap: {
routeManifest: null,
rsc: undefined,
},
functions,
};
Reflect.set(globalThis, "window", runtimeWindow);
Reflect.set(runtimeWindow, NAVIGATION_RUNTIME_KEY, runtime);
Reflect.set(runtimeWindow, NAVIGATION_RUNTIME_KEY, {
bootstrap: {
routeManifest: null,
rsc: undefined,
},
functions: {
navigate: "not callable",
},
});
expect(getNavigationRuntime()).toBeNull();
});
it("rejects route manifests without the map-backed segment graph contract", () => {
const runtimeWindow = {};
Reflect.set(globalThis, "window", runtimeWindow);
Reflect.set(runtimeWindow, NAVIGATION_RUNTIME_KEY, {
bootstrap: {
routeManifest: {
graphVersion: "test",
segmentGraph: {
interceptions: {
values: () => [],
},
},
},
rsc: undefined,
},
functions: {},
});
expect(getNavigationRuntime()).toBeNull();
});
it("rejects route manifests with malformed interception entries", () => {
const runtimeWindow = {};
const segmentGraphMaps = {
boundaries: new Map(),
defaults: new Map(),
interceptions: new Map([["bad", {}]]),
interceptionsBySlotId: new Map(),
layouts: new Map(),
pages: new Map(),
rootBoundaries: new Map(),
routeHandlers: new Map(),
routes: new Map(),
slotBindings: new Map(),
slots: new Map(),
templates: new Map(),
};
Reflect.set(globalThis, "window", runtimeWindow);
Reflect.set(runtimeWindow, NAVIGATION_RUNTIME_KEY, {
bootstrap: {
routeManifest: {
graphVersion: "test",
segmentGraph: segmentGraphMaps,
},
rsc: undefined,
},
functions: {},
});
expect(getNavigationRuntime()).toBeNull();
});
});