Files
cloudflare__vinext/tests/app-browser-entry.test.ts
T
James Anderson af7bb923ce fix(app-router): preserve page result render ordering (#2760)
* fix(app-router): preserve page result render ordering

* fix(app-router): gate page results on resolved layouts

* fix(app-router): handle exotic layout components

* fix(app-router): republish consumed prefetches promptly

* fix(app-router): normalize consumed prefetch snapshots

* fix(app-router): match Flight forward ref arguments

* fix(app-router): gate exotic page initialization

* fix(app-router): restore segment cache stale windows

* fix(app-router): separate committed stale windows

* fix(app-router): retain cache life through stream completion
2026-07-30 23:57:47 +01:00

8997 lines
308 KiB
TypeScript

import React from "react";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import {
createDevOnCaughtError,
createOnUncaughtError,
createProdOnCaughtError,
prodOnCaughtError,
prodOnRecoverableError,
} from "../packages/vinext/src/server/app-browser-error.js";
import {
clearAppNavigationFailureTarget,
handleAppNavigationFailure,
stageAppNavigationFailureTarget,
} from "../packages/vinext/src/client/app-nav-failure-handler.js";
import { applyServerActionResultDecision } from "../packages/vinext/src/server/app-browser-server-action-navigation.js";
import {
createDiscardedServerActionRefreshScheduler,
createServerActionInitiationSnapshot,
createServerActionResultFacts,
normalizeServerActionThrownValue,
parseServerActionRevalidationHeader,
readInvalidServerActionResponseError,
resolveServerActionOperationLane,
shouldClearClientNavigationCachesForServerActionResult,
shouldSyncServerActionHttpFallbackHead,
shouldScheduleRefreshForDiscardedServerAction,
} from "../packages/vinext/src/server/app-browser-action-result.js";
import {
RSC_FORM_STATE_GLOBAL,
consumeInitialFormState,
createVinextHydrateRootOptions,
hydrateRootInTransition,
} from "../packages/vinext/src/server/app-browser-hydration.js";
import { createAppBrowserNavigationController } from "../packages/vinext/src/server/app-browser-navigation-controller.js";
import {
peekSettledPrefetchResponseForNavigation,
preserveCommittedPrefetchExpiry,
prepareConsumedPrefetchResponseForPublication,
resolvePrefetchNavigationResponseUrl,
} from "../packages/vinext/src/server/app-browser-prefetch-response.js";
import { createVisitedResponseCacheEntry } from "../packages/vinext/src/server/app-visited-response-cache.js";
import {
createPopstateRestoreHandler,
restoreSynchronousPopstateScrollPosition,
} from "../packages/vinext/src/server/app-browser-popstate.js";
import {
VINEXT_RSC_COMPATIBILITY_ID_HEADER,
resolveRscCompatibilityNavigationDecision,
} from "../packages/vinext/src/server/app-rsc-cache-busting.js";
import {
isInlineCssStylesheetLinkElement,
removeStylesheetLinksCoveredByInlineCss,
} from "../packages/vinext/src/server/app-inline-css-client.js";
import {
DEV_ERROR_OVERLAY_HOST_ID,
DEV_ERROR_OVERLAY_MOUNT_ID,
createDevErrorOverlayMountNode,
createViteOpenInEditorUrl,
devOnCaughtError,
devOnUncaughtError,
formatErrorInfoForClipboard,
formatOverlayDisplayFile,
formatViteOpenInEditorFile,
installReactRefreshErrorRecovery,
normalizeViteHmrError,
} from "../packages/vinext/src/client/dev-error-overlay.js";
import {
dismissOverlay,
reportToOverlay,
subscribeOverlay,
} from "../packages/vinext/src/client/dev-error-overlay-store.js";
import { VINEXT_DEV_ERROR_RECOVERY_EVENT } from "../packages/vinext/src/utils/dev-error-recovery-event.js";
import {
APP_CACHE_ENTRY_REUSE_PROOF_KEY,
APP_ARTIFACT_COMPATIBILITY_KEY,
AppElementsWire,
APP_LAYOUT_FLAGS_KEY,
APP_ROOT_LAYOUT_KEY,
APP_ROUTE_KEY,
APP_BFCACHE_SEGMENT_IDENTITIES_KEY,
APP_SKIPPED_LAYOUT_IDS_KEY,
UNMATCHED_SLOT,
getMountedSlotIds,
getMountedSlotIdsHeader,
normalizeAppElements,
type AppElements,
type AppElementsInterception,
type AppElementsSlotBinding,
} from "../packages/vinext/src/server/app-elements.js";
import { createClientNavigationRenderSnapshot } from "../packages/vinext/src/shims/navigation.js";
import {
beginAppRouterScrollIntent,
clearAppRouterScrollIntent,
} from "../packages/vinext/src/shims/app-router-scroll-state.js";
import * as navigationShim from "../packages/vinext/src/shims/navigation.js";
import {
createBfcacheSegmentIdentityMap,
createHistoryStateWithNavigationMetadata,
createHistoryStateWithPreviousNextUrl,
createInitialBfcacheIdMap,
createNextBfcacheIdMap,
FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
VISITED_CACHE_APP_NAVIGATION_PAYLOAD_ORIGIN,
createPendingNavigationCommit,
isCompleteAppPayloadMetadata,
isCacheRestorableAppPayloadMetadata,
isHistoryStateBfcacheVersionCurrent,
readHistoryStateBfcacheIds,
readHistoryStateBfcacheVersion,
readHistoryStatePreviousNextUrl,
readHistoryStateTraversalIndex,
resolveInterceptionContextFromPreviousNextUrl,
resolveHistoryTraversalIntent,
resolveServerActionRequestState,
resolvePendingNavigationCommitDispositionDecision,
type AppRouterState,
type OperationLane,
} from "../packages/vinext/src/server/app-browser-state.js";
import { createInitialBfcacheMaps } from "../packages/vinext/src/server/app-bfcache-identity.js";
import { createArtifactCompatibilityEnvelope } from "../packages/vinext/src/server/artifact-compatibility.js";
import {
HistoryStateSnapshotCache,
RestorableClientStateController,
} from "../packages/vinext/src/server/app-history-state.js";
import {
blockDangerousStreamedRscRedirect,
resolveRscRedirectLifecycleHop,
resolveStreamedRscRedirectLifecycleHop,
} from "../packages/vinext/src/server/app-browser-rsc-redirect.js";
import {
applyApprovedVisibleCommit,
approveHmrVisibleCommit,
approvePendingNavigationCommit,
resolveAndClassifyNavigationCommit,
} from "../packages/vinext/src/server/app-browser-visible-commit.js";
import {
NAVIGATION_TRACE_SCHEMA_VERSION,
NavigationTraceReasonCodes,
NavigationTraceTransactionCodes,
createNavigationTrace,
} from "../packages/vinext/src/server/navigation-trace.js";
import { navigationPlanner } from "../packages/vinext/src/server/navigation-planner.js";
import { createCacheEntryReuseProof } from "../packages/vinext/src/server/cache-proof.js";
import {
ACTION_REVALIDATED_HEADER,
ACTION_REDIRECT_HEADER,
ACTION_REDIRECT_TYPE_HEADER,
VINEXT_MOUNTED_SLOTS_HEADER,
VINEXT_PARAMS_HEADER,
} from "../packages/vinext/src/server/headers.js";
import type {
GraphVersion,
RootBoundaryId,
RouteManifest,
RouteManifestInterception,
RouteManifestRootBoundary,
RouteManifestRoute,
RouteManifestSlotBinding,
StaticSegmentGraph,
} from "../packages/vinext/src/routing/app-route-graph.js";
type TestRouteManifestRoute = {
id?: string;
interceptions?: readonly TestRouteManifestInterception[];
layoutIds: readonly string[];
pattern: string;
rootBoundaryId: string | null;
slotBindings?: readonly AppElementsSlotBinding[];
};
type TestRouteManifestInterception = {
ownerLayoutId: string | null;
slotId: string;
sourcePattern: string;
targetPattern: string;
};
function createTestBfcacheSegmentIdentities(
entries: Record<string, unknown>,
layoutIds: readonly string[],
): Record<string, string> {
const identities: Record<string, string> = {};
for (const id of [...layoutIds, ...Object.keys(entries)]) {
const parsed = AppElementsWire.parseElementKey(id);
if (parsed?.kind === "layout" || parsed?.kind === "template") {
identities[id] = `test-identity:${id}`;
} else if (parsed?.kind === "page") {
identities[id] = `test-identity:${id}`;
} else if (parsed?.kind === "slot") {
identities[id] = `test-identity:${id}`;
}
}
return identities;
}
function createResolvedElements(
routeId: string,
rootLayoutTreePath: string | null,
interceptionContext: string | null = null,
extraEntries: Record<string, unknown> = {},
layoutIds: readonly string[] = rootLayoutTreePath === null
? []
: [AppElementsWire.encodeLayoutId(rootLayoutTreePath)],
slotBindings: readonly AppElementsSlotBinding[] = [],
interception: AppElementsInterception | null = null,
sourcePage: string | null = null,
) {
const bfcacheIdentityEntries = Object.hasOwn(extraEntries, APP_BFCACHE_SEGMENT_IDENTITIES_KEY)
? {}
: {
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: createTestBfcacheSegmentIdentities(
extraEntries,
layoutIds,
),
};
return normalizeAppElements({
...AppElementsWire.createMetadataEntries({
interception,
interceptionContext,
layoutIds,
rootLayoutTreePath,
routeId,
slotBindings,
sourcePage,
}),
...bfcacheIdentityEntries,
...extraEntries,
});
}
function createState(overrides: Partial<AppRouterState> = {}): AppRouterState {
return {
bfcacheIds: {},
elements: createResolvedElements("route:/initial", "/"),
interception: null,
layoutIds: [AppElementsWire.encodeLayoutId("/")],
layoutFlags: {},
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/initial", {}),
renderId: 0,
activeOperation: null,
interceptionContext: null,
previousNextUrl: null,
rootLayoutTreePath: "/",
routeId: "route:/initial",
slotBindings: [],
visibleCommitVersion: 0,
...overrides,
};
}
function createInterceptionProof(
sourceMatchedUrl: string,
targetMatchedUrl: string,
slotId: string = AppElementsWire.encodeSlotId("modal", sourceMatchedUrl),
): AppElementsInterception {
return {
sourceMatchedUrl,
sourceRouteId: AppElementsWire.encodeRouteId(sourceMatchedUrl, null),
slotId,
targetMatchedUrl,
targetRouteId: AppElementsWire.encodeRouteId(targetMatchedUrl, null),
};
}
function createTestRouteManifest(routes: readonly TestRouteManifestRoute[]): RouteManifest {
const manifestRoutes = new Map<string, RouteManifestRoute>();
const slotBindings = new Map<string, RouteManifestSlotBinding>();
const interceptions = new Map<string, RouteManifestInterception>();
const interceptionsBySlotId = new Map<string, RouteManifestInterception[]>();
const rootBoundaries = new Map<RootBoundaryId, RouteManifestRootBoundary>();
const routeIdByPattern = new Map(
routes.map((route) => [route.pattern, route.id ?? `route:${route.pattern}`]),
);
for (const route of routes) {
const routeId = route.id ?? `route:${route.pattern}`;
const rootBoundaryId =
route.rootBoundaryId === null ? null : (route.rootBoundaryId as RootBoundaryId);
const routeSlotBindings = route.slotBindings ?? [];
const slotIds = routeSlotBindings.map((binding) => binding.slotId).sort();
const patternParts = route.pattern.split("/").filter((segment) => segment.length > 0);
manifestRoutes.set(routeId, {
id: routeId,
isDynamic: patternParts.some((part) => part.startsWith(":")),
layoutIds: route.layoutIds,
pageId: null,
paramNames: patternParts
.filter((part) => part.startsWith(":"))
.map((part) => part.replace(/^:/, "").replace(/[+*]$/, "")),
pattern: route.pattern,
patternParts,
rootBoundaryId,
rootParamNames: [],
routeHandlerId: null,
slotIds,
templateIds: [],
});
for (const binding of routeSlotBindings) {
slotBindings.set(`${routeId}::${binding.slotId}`, {
defaultId: binding.state === "default" ? `default:${binding.slotId}` : null,
id: `${routeId}::${binding.slotId}`,
ownerLayoutId: binding.ownerLayoutId,
routeId,
routeSegments: null,
slotId: binding.slotId,
state: binding.state,
});
}
for (const interception of route.interceptions ?? []) {
const targetPatternParts = interception.targetPattern
.split("/")
.filter((segment) => segment.length > 0);
const id = `interception:${interception.slotId}:${interception.sourcePattern}->${interception.targetPattern}`;
const manifestInterception = {
id,
interceptingRouteId: routeIdByPattern.get(interception.sourcePattern) ?? null,
ownerLayoutId: interception.ownerLayoutId,
slotId: interception.slotId,
sourcePattern: interception.sourcePattern,
sourcePatternParts: interception.sourcePattern
.split("/")
.filter((segment) => segment.length > 0),
targetPattern: interception.targetPattern,
targetPatternParts,
targetRouteId: routeIdByPattern.get(interception.targetPattern) ?? null,
};
interceptions.set(id, manifestInterception);
const slotInterceptions = interceptionsBySlotId.get(interception.slotId);
if (slotInterceptions) {
slotInterceptions.push(manifestInterception);
} else {
interceptionsBySlotId.set(interception.slotId, [manifestInterception]);
}
}
const rootLayoutId = route.layoutIds[0];
if (rootBoundaryId !== null && rootLayoutId !== undefined) {
rootBoundaries.set(rootBoundaryId, {
id: rootBoundaryId,
layoutId: rootLayoutId,
treePath: route.rootBoundaryId?.replace(/^root-boundary:/, "") ?? "/",
});
}
}
const segmentGraph: StaticSegmentGraph = {
boundaries: new Map(),
defaults: new Map(),
interceptions,
interceptionsBySlotId,
layouts: new Map(),
pages: new Map(),
rootBoundaries,
routeHandlers: new Map(),
routes: manifestRoutes,
slotBindings,
slots: new Map(),
templates: new Map(),
};
return {
graphVersion: "graph:test" as GraphVersion,
segmentGraph,
};
}
function rootBoundaryIdFromTreePath(rootLayoutTreePath: string | null): string | null {
return rootLayoutTreePath === null ? null : `root-boundary:${rootLayoutTreePath}`;
}
function normalizeTestRoutePattern(pathname: string): string {
try {
return decodeURI(pathname);
} catch {
return pathname;
}
}
function routeIdWithoutInterceptionContext(routeId: string): string {
const parsed = AppElementsWire.parseElementKey(routeId);
if (parsed?.kind !== "route") return routeId;
return AppElementsWire.encodeRouteId(parsed.path, null);
}
function createRouteManifestForPendingCommit(
currentState: AppRouterState,
pending: Awaited<ReturnType<typeof createPendingNavigationCommit>>,
): RouteManifest {
const currentPattern =
currentState.interception?.sourceMatchedUrl ??
normalizeTestRoutePattern(currentState.navigationSnapshot.pathname);
const currentRouteId =
currentState.interception?.sourceRouteId ??
routeIdWithoutInterceptionContext(currentState.routeId);
const currentRoute = {
id: currentRouteId,
layoutIds: currentState.layoutIds,
pattern: currentPattern,
rootBoundaryId: rootBoundaryIdFromTreePath(currentState.rootLayoutTreePath),
slotBindings: currentState.slotBindings,
};
const pendingInterception = pending.action.interception;
if (pendingInterception !== null) {
const currentMatchesSource =
currentRouteId === pendingInterception.sourceRouteId &&
currentPattern === pendingInterception.sourceMatchedUrl;
const sourceLayoutIds = currentMatchesSource
? currentState.layoutIds
: pending.action.layoutIds;
const sourceRootLayoutTreePath = currentMatchesSource
? currentState.rootLayoutTreePath
: pending.rootLayoutTreePath;
return createTestRouteManifest([
currentRoute,
{
id: pendingInterception.sourceRouteId,
interceptions: [
{
ownerLayoutId:
pending.action.slotBindings.find(
(binding) => binding.slotId === pendingInterception.slotId,
)?.ownerLayoutId ?? null,
slotId: pendingInterception.slotId,
sourcePattern: pendingInterception.sourceMatchedUrl,
targetPattern: pendingInterception.targetMatchedUrl,
},
],
layoutIds: sourceLayoutIds,
pattern: pendingInterception.sourceMatchedUrl,
rootBoundaryId: rootBoundaryIdFromTreePath(sourceRootLayoutTreePath),
slotBindings: pending.action.slotBindings,
},
{
id: pendingInterception.targetRouteId,
layoutIds: pending.action.layoutIds,
pattern: pendingInterception.targetMatchedUrl,
rootBoundaryId: rootBoundaryIdFromTreePath(pending.rootLayoutTreePath),
},
]);
}
return createTestRouteManifest([
currentRoute,
{
id: routeIdWithoutInterceptionContext(pending.routeId),
layoutIds: pending.action.layoutIds,
pattern: normalizeTestRoutePattern(pending.action.navigationSnapshot.pathname),
rootBoundaryId: rootBoundaryIdFromTreePath(pending.rootLayoutTreePath),
slotBindings: pending.action.slotBindings,
},
]);
}
function createRootChangeRouteManifest(
options: {
currentPattern?: string;
currentRouteId?: string;
targetPattern?: string;
targetRouteId?: string;
} = {},
): RouteManifest {
return createTestRouteManifest([
{
id: options.currentRouteId ?? "route:/initial",
layoutIds: [AppElementsWire.encodeLayoutId("/(marketing)")],
pattern: options.currentPattern ?? "/initial",
rootBoundaryId: "root-boundary:/(marketing)",
},
{
id: options.targetRouteId ?? "route:/dashboard",
layoutIds: [AppElementsWire.encodeLayoutId("/(dashboard)")],
pattern: options.targetPattern ?? "/dashboard",
rootBoundaryId: "root-boundary:/(dashboard)",
},
]);
}
type TestPendingDispositionOptions = {
activeNavigationId: number;
currentRootLayoutTreePath: string | null;
currentVisibleCommitVersion: number;
nextRootLayoutTreePath: string | null;
renderId?: number;
startedNavigationId: number;
startedVisibleCommitVersion: number;
};
async function resolveTestPendingNavigationCommitDispositionDecision(
options: TestPendingDispositionOptions,
) {
const startState = createState({
rootLayoutTreePath: options.currentRootLayoutTreePath,
visibleCommitVersion: options.startedVisibleCommitVersion,
});
const currentState = createState({
rootLayoutTreePath: options.currentRootLayoutTreePath,
visibleCommitVersion: options.currentVisibleCommitVersion,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: startState,
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", options.nextRootLayoutTreePath),
),
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/dashboard", {}),
operationLane: "navigation",
renderId: options.renderId ?? options.startedNavigationId,
type: "navigate",
});
return resolvePendingNavigationCommitDispositionDecision({
activeNavigationId: options.activeNavigationId,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: options.startedNavigationId,
});
}
function createControllerHarness(
initialState: AppRouterState = createState(),
deps?: Parameters<typeof createAppBrowserNavigationController>[0],
) {
const controller = createAppBrowserNavigationController(deps);
const stateRef: { current: AppRouterState } = { current: initialState };
let resolveNextVisibleCommitDispatch: (() => void) | null = null;
const setBrowserRouterState = vi.fn((value: AppRouterState | Promise<AppRouterState>) => {
if (!(value instanceof Promise)) {
stateRef.current = value;
const resolveDispatch = resolveNextVisibleCommitDispatch;
resolveNextVisibleCommitDispatch = null;
resolveDispatch?.();
}
});
const detach = controller.attachBrowserRouterState(setBrowserRouterState, stateRef);
const waitForNextVisibleCommitDispatch = (): Promise<void> => {
if (resolveNextVisibleCommitDispatch !== null) {
throw new Error("Already waiting for the next visible commit dispatch");
}
return new Promise((resolve) => {
resolveNextVisibleCommitDispatch = resolve;
});
};
return {
controller,
detach,
setBrowserRouterState,
stateRef,
waitForNextVisibleCommitDispatch,
};
}
type TestBrowserNavigationController = ReturnType<typeof createAppBrowserNavigationController>;
type TestBrowserNavigationPayloadOptions = Parameters<
TestBrowserNavigationController["renderNavigationPayload"]
>[0];
function renderCurrentStateNavigationPayload(
controller: TestBrowserNavigationController,
options: Omit<TestBrowserNavigationPayloadOptions, "navigationInitiationState">,
) {
return controller.renderNavigationPayload({
...options,
navigationInitiationState: controller.getBrowserRouterState(),
});
}
async function dispatchTestNavigationPayload(options: {
controller: ReturnType<typeof createAppBrowserNavigationController>;
navigationCommitKind: "authoritative" | "detached";
navigationInitiationState: AppRouterState;
navigationSnapshot: AppRouterState["navigationSnapshot"];
navId: number;
nextElements: Promise<AppElements>;
params: Record<string, string | string[]>;
targetHref: string;
waitForNextVisibleCommitDispatch: () => Promise<void>;
}): Promise<void> {
const commitDispatched = options.waitForNextVisibleCommitDispatch();
void options.controller.renderNavigationPayload({
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationCommitKind: options.navigationCommitKind,
navigationInitiationState: options.navigationInitiationState,
navigationSnapshot: options.navigationSnapshot,
navId: options.navId,
nextElements: options.nextElements,
operationLane: "navigation",
params: options.params,
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
pendingRouterState: null,
previousNextUrl: null,
targetHref: options.targetHref,
});
await commitDispatched;
}
type ApprovedTestCommitOptions = {
activeNavigationId?: number;
extraEntries?: Record<string, unknown>;
interception?: AppElementsInterception | null;
interceptionContext?: string | null;
layoutIds?: readonly string[];
layoutFlags?: AppRouterState["layoutFlags"];
navigationSnapshot?: AppRouterState["navigationSnapshot"];
operationLane?: OperationLane;
previousNextUrl?: string | null;
renderId?: number;
rootLayoutTreePath: string | null;
routeId: string;
slotBindings?: readonly AppElementsSlotBinding[];
startedNavigationId?: number;
targetHref?: string;
type?: "navigate" | "replace" | "traverse";
};
async function applyApprovedTestCommit(
state: AppRouterState,
options: ApprovedTestCommitOptions,
): Promise<AppRouterState> {
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: state,
nextElements: Promise.resolve(
createResolvedElements(
options.routeId,
options.rootLayoutTreePath,
options.interceptionContext ?? null,
{
[APP_LAYOUT_FLAGS_KEY]: options.layoutFlags ?? {},
...options.extraEntries,
},
options.layoutIds,
options.slotBindings,
options.interception ?? null,
),
),
navigationSnapshot: options.navigationSnapshot ?? state.navigationSnapshot,
operationLane: options.operationLane ?? "navigation",
previousNextUrl: options.previousNextUrl,
renderId: options.renderId ?? 1,
type: options.type ?? "navigate",
});
const activeNavigationId = options.activeNavigationId ?? 1;
const approval = approvePendingNavigationCommit({
activeNavigationId,
currentState: state,
pending,
routeManifest: createRouteManifestForPendingCommit(state, pending),
startedNavigationId: options.startedNavigationId ?? activeNavigationId,
targetHref: options.targetHref ?? "https://example.com/initial",
});
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
return applyApprovedVisibleCommit(state, approval.approvedCommit);
}
function stubWindow(href: string) {
const assign = vi.fn();
const replace = vi.fn();
const storage = new Map<string, string>();
vi.stubGlobal("window", {
history: { state: null },
location: {
assign,
href,
origin: new URL(href).origin,
replace,
},
sessionStorage: {
getItem(key: string) {
return storage.get(key) ?? null;
},
removeItem(key: string) {
storage.delete(key);
},
setItem(key: string, value: string) {
storage.set(key, value);
},
},
});
return { assign, replace, storage };
}
function createDeferred(): { resolve: () => void; promise: Promise<void> } {
let resolve: () => void = () => {
throw new Error("Promise was not initialized");
};
const promise = new Promise<void>((resolveInner) => {
resolve = resolveInner;
});
return { promise, resolve };
}
type TestDomElement = {
attributes: Record<string, string>;
removed: boolean;
getAttribute(name: string): string | null;
hasAttribute(name: string): boolean;
remove(): void;
};
function createTestDomElement(attributes: Record<string, string>): TestDomElement {
return {
attributes,
removed: false,
getAttribute(name) {
return Object.prototype.hasOwnProperty.call(this.attributes, name)
? (this.attributes[name] ?? "")
: null;
},
hasAttribute(name) {
return Object.prototype.hasOwnProperty.call(this.attributes, name);
},
remove() {
this.removed = true;
},
};
}
function installInlineCssCleanupDocument(options: {
links: TestDomElement[];
styles: TestDomElement[];
}): void {
const { links, styles } = options;
const head = {
querySelectorAll<T extends Element>(selector: string): T[] {
if (selector === "style[data-vinext-inline-css][data-href]") {
const matchingStyles = styles.filter(
(style) =>
style.hasAttribute("data-vinext-inline-css") && style.hasAttribute("data-href"),
);
return matchingStyles as unknown as T[];
}
if (selector === 'link[rel="stylesheet"][href][data-precedence]') {
const matchingLinks = links.filter(
(link) =>
link.getAttribute("rel") === "stylesheet" &&
link.hasAttribute("href") &&
link.hasAttribute("data-precedence"),
);
return matchingLinks as unknown as T[];
}
if (selector === "link[rel][href]") {
const matchingLinks = links.filter(
(link) => link.hasAttribute("rel") && link.hasAttribute("href"),
);
return matchingLinks as unknown as T[];
}
throw new Error(`Unexpected selector: ${selector}`);
},
};
vi.stubGlobal("document", { head });
}
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
describe("app browser entry inline CSS cleanup", () => {
it("classifies stylesheet links with tokenized rel values and legacy precedence", () => {
expect(
isInlineCssStylesheetLinkElement(
createTestDomElement({
"data-precedence": "next",
href: "/_next/static/app.css",
rel: "preload stylesheet",
}),
),
).toBe(true);
expect(
isInlineCssStylesheetLinkElement(
createTestDomElement({
href: "/_next/static/legacy.css",
precedence: "next",
rel: "stylesheet",
}),
),
).toBe(true);
expect(
isInlineCssStylesheetLinkElement(
createTestDomElement({
href: "/_next/static/preload.css",
rel: "preload",
}),
),
).toBe(false);
});
it("prunes navigated stylesheet links covered by inline CSS", () => {
const tokenizedRelLink = createTestDomElement({
"data-precedence": "next",
href: "/_next/static/app.css",
rel: "preload stylesheet",
});
const legacyPrecedenceLink = createTestDomElement({
href: "/_next/static/legacy.css",
precedence: "next",
rel: "stylesheet",
});
const uncoveredLink = createTestDomElement({
"data-precedence": "next",
href: "/_next/static/uncovered.css",
rel: "stylesheet",
});
installInlineCssCleanupDocument({
links: [tokenizedRelLink, legacyPrecedenceLink, uncoveredLink],
styles: [
createTestDomElement({
"data-href": "/_next/static/app.css /_next/static/legacy.css",
"data-vinext-inline-css": "",
}),
],
});
removeStylesheetLinksCoveredByInlineCss();
expect(tokenizedRelLink.removed).toBe(true);
expect(legacyPrecedenceLink.removed).toBe(true);
expect(uncoveredLink.removed).toBe(false);
});
});
describe("app browser entry navigation scheduling", () => {
it("peeks at a settled prefetch without transferring ownership before reuse planning", () => {
const snapshot = {
buffer: new ArrayBuffer(0),
contentType: "text/x-component",
expiresAt: 123,
mountedSlotsHeader: null,
paramsHeader: null,
renderedPathAndSearch: null,
url: "/source?_rsc=prefetch-digest",
};
const peek = vi.fn(() => snapshot);
expect(
peekSettledPrefetchResponseForNavigation({
additionalRscUrls: ["/source"],
bypassNavigationCache: false,
interceptionContext: "/parent",
mountedSlotsHeader: "slot:children:/target",
navigationKind: "navigate",
peek,
targetPathAndSearch: "/target",
}),
).toBe(snapshot);
expect(peek).toHaveBeenCalledWith("/target", "/parent", "slot:children:/target", {
additionalRscUrls: ["/source"],
});
});
it("normalizes consumed prefetch snapshots for committed-cache publication", () => {
const preparedElements = { route: "prepared" } as unknown as AppElements;
const publication = prepareConsumedPrefetchResponseForPublication(
{
buffer: new ArrayBuffer(0),
completedDynamicStaleTimeSeconds: 60,
contentType: "text/x-component",
dynamicStaleTimeSeconds: 300,
expiresAt: 123,
paramsHeader: null,
preparedElements,
renderedPathAndSearch: null,
url: "https://example.com/source?_rsc=prefetch-digest",
},
"/rewrite?_rsc=visible-digest",
);
expect(publication.expiresAt).toBe(123);
expect(publication.preparedElements).toBe(preparedElements);
expect(publication.snapshot).not.toHaveProperty("expiresAt");
expect(publication.snapshot).not.toHaveProperty("preparedElements");
expect(publication.snapshot.url).toBe("/rewrite?_rsc=visible-digest");
});
it("keeps runtime-prefetch and visited-cache deadlines independent after commit", () => {
const now = 1_000_000;
const publication = prepareConsumedPrefetchResponseForPublication(
{
buffer: new ArrayBuffer(0),
completedDynamicStaleTimeSeconds: 30,
contentType: "text/x-component",
dynamicStaleTimeSeconds: 30,
expiresAt: now + 240_000,
paramsHeader: null,
renderedPathAndSearch: null,
serverStaleTime: { kind: "resolved", seconds: 240 },
url: "/runtime-prefetch",
},
"/runtime-prefetch",
);
const prefetchSnapshot = preserveCommittedPrefetchExpiry(
publication.snapshot,
publication.expiresAt,
);
const visited = createVisitedResponseCacheEntry({
now,
params: {},
response: publication.snapshot,
});
expect(prefetchSnapshot.expiresAt).toBe(now + 240_000);
expect(publication.snapshot).not.toHaveProperty("expiresAt");
expect(visited.expiresAt).toBe(now + 30_000);
});
it("keeps the visible URL when a settled prefetch was found through a rewrite alias", () => {
expect(
resolvePrefetchNavigationResponseUrl({
additionalRscUrls: ["/source?_rsc=source-digest"],
origin: "https://example.com",
responseUrl: "https://example.com/source?_rsc=prefetch-digest",
visibleRscUrl: "/rewrite?_rsc=visible-digest",
}),
).toBe("/rewrite?_rsc=visible-digest");
});
it("preserves the prefetch response digest and canonicalizes same-origin URLs", () => {
expect(
resolvePrefetchNavigationResponseUrl({
additionalRscUrls: ["/source?_rsc=source-digest"],
origin: "https://example.com",
responseUrl: "https://example.com/rewrite?_rsc=prefetch-digest",
visibleRscUrl: "/rewrite?_rsc=visible-digest",
}),
).toBe("/rewrite?_rsc=prefetch-digest");
});
it("preserves cross-origin prefetch response URLs for navigation validation", () => {
expect(
resolvePrefetchNavigationResponseUrl({
additionalRscUrls: ["/rewrite?_rsc=source-digest"],
origin: "https://example.com",
responseUrl: "https://other.example/rewrite?_rsc=prefetch-digest",
visibleRscUrl: "/rewrite?_rsc=visible-digest",
}),
).toBe("https://other.example/rewrite?_rsc=prefetch-digest");
});
it("hard-navigates RSC responses when the response compatibility ID is missing or stale", () => {
stubWindow("https://example.com/current");
expect(
resolveRscCompatibilityNavigationDecision({
clientCompatibilityId: "compat-a",
currentHref: "/target?tab=1#details",
origin: "https://example.com",
responseCompatibilityId: null,
responseUrl: "https://example.com/target.rsc?tab=1&_rsc=stale",
}),
).toEqual({
hardNavigationTarget: "/target?tab=1#details",
kind: "hard-navigate",
});
expect(
resolveRscCompatibilityNavigationDecision({
clientCompatibilityId: "compat-a",
currentHref: "/target/",
origin: "https://example.com",
responseCompatibilityId: "compat-b",
responseUrl: "https://example.com/target.rsc?_rsc=stale",
}),
).toEqual({
hardNavigationTarget: "/target/",
kind: "hard-navigate",
});
});
it("keeps RSC responses on the soft-navigation path when compatibility IDs match", () => {
stubWindow("https://example.com/current");
expect(
resolveRscCompatibilityNavigationDecision({
clientCompatibilityId: "compat-a",
currentHref: "/target",
origin: "https://example.com",
responseCompatibilityId: "compat-a",
responseUrl: "https://example.com/target.rsc?_rsc=fresh",
}),
).toEqual({ kind: "compatible" });
});
it("createServerActionResultFacts normalises raw response data into planner facts", () => {
const currentHref = "https://example.com/current";
// RSC redirect with push type
const pushFacts = createServerActionResultFacts({
actionRedirectHref: "https://example.com/target",
actionRedirectType: "push",
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
contentTypeHeader: "text/x-component",
currentHref,
origin: "https://example.com",
responseUrl: currentHref,
});
expect(pushFacts.actionRedirectHref).toBe("https://example.com/target");
expect(pushFacts.actionRedirectType).toBe("push");
expect(pushFacts.isRscContentType).toBe(true);
// RSC redirect with unknown type normalises to replace
const replaceFacts = createServerActionResultFacts({
actionRedirectHref: "https://example.com/target",
actionRedirectType: "unknown",
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
contentTypeHeader: "text/x-component",
currentHref,
origin: "https://example.com",
responseUrl: currentHref,
});
expect(replaceFacts.actionRedirectType).toBe("replace");
// No redirect — the raw type is still normalized for the planner contract
const noRedirectFacts = createServerActionResultFacts({
actionRedirectHref: null,
actionRedirectType: null,
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
contentTypeHeader: "text/x-component",
currentHref,
origin: "https://example.com",
responseUrl: currentHref,
});
expect(noRedirectFacts.actionRedirectHref).toBeNull();
expect(noRedirectFacts.actionRedirectType).toBe("replace");
// Non-RSC response — isRscContentType should be false
const nonRscFacts = createServerActionResultFacts({
actionRedirectHref: null,
actionRedirectType: null,
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
contentTypeHeader: "text/plain",
currentHref,
origin: "https://example.com",
responseUrl: currentHref,
});
expect(nonRscFacts.isRscContentType).toBe(false);
});
it("creates replayable cached RSC snapshots with compatibility IDs", async () => {
stubWindow("https://example.com/current");
const responseUrl = "https://example.com/target.rsc?_rsc=fresh";
const snapshot = navigationShim.createCachedRscResponseSnapshot(
new Response("unused", {
headers: {
"content-type": "text/x-component",
[VINEXT_MOUNTED_SLOTS_HEADER]: "children",
[VINEXT_PARAMS_HEADER]: "%7B%22slug%22%3A%22target%22%7D",
[VINEXT_RSC_COMPATIBILITY_ID_HEADER]: "compat-a",
},
}),
await new Response("flight").arrayBuffer(),
responseUrl,
);
expect(snapshot.compatibilityIdHeader).toBe("compat-a");
expect(snapshot.url).toBe(responseUrl);
expect(
resolveRscCompatibilityNavigationDecision({
clientCompatibilityId: "compat-a",
currentHref: "/target",
origin: "https://example.com",
responseCompatibilityId: snapshot.compatibilityIdHeader,
responseUrl: snapshot.url,
}),
).toEqual({ kind: "compatible" });
const replayed = navigationShim.restoreRscResponse(snapshot);
expect(replayed.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER)).toBe("compat-a");
expect(replayed.headers.get(VINEXT_MOUNTED_SLOTS_HEADER)).toBe("children");
expect(replayed.headers.get(VINEXT_PARAMS_HEADER)).toBe("%7B%22slug%22%3A%22target%22%7D");
expect(await replayed.text()).toBe("flight");
});
it("parses action revalidation headers into explicit client effects", () => {
expect(parseServerActionRevalidationHeader(new Headers())).toBe("none");
expect(
parseServerActionRevalidationHeader(new Headers({ [ACTION_REVALIDATED_HEADER]: "1" })),
).toBe("staticAndDynamic");
expect(
parseServerActionRevalidationHeader(new Headers({ [ACTION_REVALIDATED_HEADER]: "2" })),
).toBe("dynamicOnly");
expect(
parseServerActionRevalidationHeader(
new Headers({ [ACTION_REVALIDATED_HEADER]: JSON.stringify("1") }),
),
).toBe("none");
expect(
parseServerActionRevalidationHeader(new Headers({ [ACTION_REVALIDATED_HEADER]: "not-json" })),
).toBe("none");
expect(resolveServerActionOperationLane("none")).toBe("server-action");
expect(resolveServerActionOperationLane("dynamicOnly")).toBe("refresh");
expect(resolveServerActionOperationLane("staticAndDynamic")).toBe("refresh");
});
it("restores action HTTP fallback errors from response status", () => {
// Ported from Next.js: test/e2e/app-dir/actions/app-action.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/actions/app-action.test.ts
const fallback = normalizeServerActionThrownValue(new Error("sanitized"), 404);
expect(fallback).toBeInstanceOf(Error);
if (fallback instanceof Error && "digest" in fallback) {
expect(fallback.digest).toBe("NEXT_HTTP_ERROR_FALLBACK;404");
} else {
throw new Error("Expected fallback to have a digest property");
}
});
it("lets thrown action HTTP fallbacks own their boundary robots metadata", () => {
expect(
shouldSyncServerActionHttpFallbackHead({
returnValue: { ok: false, data: new Error("sanitized") },
}),
).toBe(false);
expect(shouldSyncServerActionHttpFallbackHead({ returnValue: { ok: true, data: null } })).toBe(
true,
);
expect(
shouldSyncServerActionHttpFallbackHead({
root: { __route: "/current" },
returnValue: { ok: false, data: new Error("sanitized") },
}),
).toBe(false);
});
it("preserves ordinary server action errors for 500 responses", () => {
const error = new Error("sanitized action failure");
expect(normalizeServerActionThrownValue(error, 500)).toBe(error);
});
it("uses text/plain action response bodies as boundary errors", async () => {
// Ported from Next.js: test/e2e/app-dir/actions/app-action.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/actions/app-action.test.ts
const error = await readInvalidServerActionResponseError(
new Response("Custom error!", {
status: 500,
headers: { "content-type": "text/plain;charset=utf-8" },
}),
false,
);
expect(error?.message).toBe("Custom error!");
});
it("applyServerActionResultDecision clears caches and hard-navigates for a redirect mismatch", () => {
// Executor regression: verifies the wiring between classifyServerActionResult output and
// the executor's performHardNavigation + clearClientNavigationCaches dispatch.
const clearCaches = vi.fn();
const performHardNavigation = vi.fn();
// push redirect with an incompatible build — executor should hard-navigate and clear caches
const pushDecision = navigationPlanner.classifyServerActionResult({
actionRedirectHref: "https://example.com/target?tab=1",
actionRedirectType: "push",
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
currentHref: "https://example.com/current",
isRscContentType: true,
origin: "https://example.com",
responseUrl: "https://example.com/current",
});
expect(applyServerActionResultDecision(pushDecision, clearCaches, performHardNavigation)).toBe(
true,
);
expect(clearCaches).toHaveBeenCalledOnce();
expect(performHardNavigation).toHaveBeenCalledWith(
"https://example.com/target?tab=1",
"assign",
);
clearCaches.mockClear();
performHardNavigation.mockClear();
// replace redirect with an incompatible build — executor should use replace history mode
const replaceDecision = navigationPlanner.classifyServerActionResult({
actionRedirectHref: "https://example.com/replaced",
actionRedirectType: "replace",
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
currentHref: "https://example.com/current",
isRscContentType: true,
origin: "https://example.com",
responseUrl: "https://example.com/current",
});
expect(
applyServerActionResultDecision(replaceDecision, clearCaches, performHardNavigation),
).toBe(true);
expect(performHardNavigation).toHaveBeenCalledWith("https://example.com/replaced", "replace");
clearCaches.mockClear();
performHardNavigation.mockClear();
// no-redirect RSC mismatch — executor reloads current href, does NOT clear caches
const noRedirectDecision = navigationPlanner.classifyServerActionResult({
actionRedirectHref: null,
actionRedirectType: "replace",
clientCompatibilityId: "client-build",
compatibilityIdHeader: "server-build",
currentHref: "https://example.com/dashboard?view=grid",
isRscContentType: true,
origin: "https://example.com",
responseUrl: "https://example.com/dashboard?view=grid",
});
expect(
applyServerActionResultDecision(noRedirectDecision, clearCaches, performHardNavigation),
).toBe(true);
expect(clearCaches).not.toHaveBeenCalled();
expect(performHardNavigation).toHaveBeenCalledWith(
"https://example.com/dashboard?view=grid",
undefined,
);
clearCaches.mockClear();
performHardNavigation.mockClear();
// compatible build — executor should proceed, not hard-navigate
const proceedDecision = navigationPlanner.classifyServerActionResult({
actionRedirectHref: "https://example.com/target",
actionRedirectType: "push",
clientCompatibilityId: "same-build",
compatibilityIdHeader: "same-build",
currentHref: "https://example.com/current",
isRscContentType: true,
origin: "https://example.com",
responseUrl: "https://example.com/current",
});
expect(
applyServerActionResultDecision(proceedDecision, clearCaches, performHardNavigation),
).toBe(false);
expect(clearCaches).not.toHaveBeenCalled();
expect(performHardNavigation).not.toHaveBeenCalled();
});
it("wiring: createServerActionResultFacts + classifyServerActionResult end-to-end", () => {
// Regression for the browser-entry seam: production derives facts via
// createServerActionResultFacts and passes them to the planner. This test
// exercises the real helper against synthetic responses so the seam cannot
// drift out of sync.
const currentHref = "https://example.com/current";
const origin = "https://example.com";
// Incompatible RSC action redirect — mimics the headers a real server would return.
const redirectResponse = new Response("flight", {
headers: {
"content-type": "text/x-component",
[VINEXT_RSC_COMPATIBILITY_ID_HEADER]: "server-build",
[ACTION_REDIRECT_HEADER]: "https://example.com/target",
[ACTION_REDIRECT_TYPE_HEADER]: "push",
},
});
const pushFacts = createServerActionResultFacts({
actionRedirectHref: redirectResponse.headers.get(ACTION_REDIRECT_HEADER),
actionRedirectType: redirectResponse.headers.get(ACTION_REDIRECT_TYPE_HEADER),
clientCompatibilityId: "client-build",
compatibilityIdHeader: redirectResponse.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER),
contentTypeHeader: redirectResponse.headers.get("content-type"),
currentHref,
origin,
responseUrl: currentHref,
});
const pushDecision = navigationPlanner.classifyServerActionResult(pushFacts);
expect(pushDecision.kind).toBe("hardNavigate");
if (pushDecision.kind === "hardNavigate") {
expect(pushDecision.url).toBe("https://example.com/target");
expect(pushDecision.historyMode).toBe("assign");
expect(pushDecision.clearClientNavigationCaches).toBe(true);
expect(pushDecision.reason).toBe("serverActionRedirectCompatibilityMismatch");
}
// Non-standard redirect type (e.g. misspelled header) should normalise to "replace".
const weirdRedirectResponse = new Response("flight", {
headers: {
"content-type": "text/x-component",
[VINEXT_RSC_COMPATIBILITY_ID_HEADER]: "server-build",
[ACTION_REDIRECT_HEADER]: "https://example.com/target",
[ACTION_REDIRECT_TYPE_HEADER]: "unknown",
},
});
const weirdFacts = createServerActionResultFacts({
actionRedirectHref: weirdRedirectResponse.headers.get(ACTION_REDIRECT_HEADER),
actionRedirectType: weirdRedirectResponse.headers.get(ACTION_REDIRECT_TYPE_HEADER),
clientCompatibilityId: "client-build",
compatibilityIdHeader: weirdRedirectResponse.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER),
contentTypeHeader: weirdRedirectResponse.headers.get("content-type"),
currentHref,
origin,
responseUrl: currentHref,
});
const weirdDecision = navigationPlanner.classifyServerActionResult(weirdFacts);
expect(weirdDecision.kind).toBe("hardNavigate");
if (weirdDecision.kind === "hardNavigate") {
expect(weirdDecision.url).toBe("https://example.com/target");
expect(weirdDecision.historyMode).toBe("replace");
expect(weirdDecision.clearClientNavigationCaches).toBe(true);
expect(weirdDecision.reason).toBe("serverActionRedirectCompatibilityMismatch");
}
// No-redirect incompatible RSC response — should reload current href without clearing caches.
const noRedirectResponse = new Response("flight", {
headers: {
"content-type": "text/x-component",
[VINEXT_RSC_COMPATIBILITY_ID_HEADER]: "server-build",
},
});
const noRedirectFacts = createServerActionResultFacts({
actionRedirectHref: null,
actionRedirectType: null,
clientCompatibilityId: "client-build",
compatibilityIdHeader: noRedirectResponse.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER),
contentTypeHeader: noRedirectResponse.headers.get("content-type"),
currentHref,
origin,
responseUrl: currentHref,
});
const noRedirectDecision = navigationPlanner.classifyServerActionResult(noRedirectFacts);
expect(noRedirectDecision.kind).toBe("hardNavigate");
if (noRedirectDecision.kind === "hardNavigate") {
expect(noRedirectDecision.url).toBe(currentHref);
expect(noRedirectDecision.clearClientNavigationCaches).toBe(false);
expect(noRedirectDecision.reason).toBe("serverActionRscCompatibilityMismatch");
}
// Non-RSC response should always proceed regardless of compatibility header.
const nonRscResponse = new Response(JSON.stringify({ ok: true }), {
headers: {
"content-type": "application/json",
[VINEXT_RSC_COMPATIBILITY_ID_HEADER]: "server-build",
},
});
const nonRscFacts = createServerActionResultFacts({
actionRedirectHref: null,
actionRedirectType: null,
clientCompatibilityId: "client-build",
compatibilityIdHeader: nonRscResponse.headers.get(VINEXT_RSC_COMPATIBILITY_ID_HEADER),
contentTypeHeader: nonRscResponse.headers.get("content-type"),
currentHref,
origin,
responseUrl: currentHref,
});
expect(navigationPlanner.classifyServerActionResult(nonRscFacts).kind).toBe("proceed");
});
it("uses a stable generic error for non-RSC action responses", async () => {
// Ported from Next.js: test/e2e/app-dir/actions/app-action.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/actions/app-action.test.ts
const error = await readInvalidServerActionResponseError(
new Response(JSON.stringify({ error: "Custom error!" }), {
status: 500,
headers: { "content-type": "application/json" },
}),
false,
);
expect(error?.message).toBe("An unexpected response was received from the server.");
});
it("allows non-RSC server action redirect responses", async () => {
const error = await readInvalidServerActionResponseError(
new Response("", {
status: 303,
headers: { "content-type": "text/plain" },
}),
true,
);
expect(error).toBeNull();
});
it("captures server action initiation URL state without a hash in the request path", () => {
const routerState = createState({
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/a?tab=1", {
slug: "a",
}),
routeId: "route:/a",
});
const snapshot = createServerActionInitiationSnapshot({
href: "https://example.com/a?tab=1#section",
navigationId: 42,
routerState,
});
expect(snapshot.href).toBe("https://example.com/a?tab=1#section");
expect(snapshot.navigationId).toBe(42);
expect(snapshot.path).toBe("/a?tab=1");
expect(snapshot.routerState).toBe(routerState);
});
it("keeps client navigation caches for no-root server action results", () => {
expect(
shouldClearClientNavigationCachesForServerActionResult({
returnValue: { ok: true, data: "action-result" },
}),
).toBe(false);
expect(
shouldClearClientNavigationCachesForServerActionResult({
root: createResolvedElements("route:/settings", "/"),
returnValue: { ok: true, data: "action-result" },
}),
).toBe(true);
expect(
shouldClearClientNavigationCachesForServerActionResult(
createResolvedElements("route:/settings", "/"),
),
).toBe(true);
expect(
shouldClearClientNavigationCachesForServerActionResult(
{
returnValue: { ok: true, data: "action-result" },
},
"staticAndDynamic",
),
).toBe(true);
expect(
shouldClearClientNavigationCachesForServerActionResult(
{
returnValue: { ok: true, data: "action-result" },
},
"dynamicOnly",
),
).toBe(true);
});
it("schedules discarded action refreshes only for revalidated actions", () => {
expect(shouldScheduleRefreshForDiscardedServerAction("none")).toBe(false);
expect(shouldScheduleRefreshForDiscardedServerAction("dynamicOnly")).toBe(true);
expect(shouldScheduleRefreshForDiscardedServerAction("staticAndDynamic")).toBe(true);
});
it("coalesces discarded action refreshes until active navigation settles", () => {
const queued: Array<() => void> = [];
const runRefresh = vi.fn();
const scheduler = createDiscardedServerActionRefreshScheduler({
queueTask(callback) {
queued.push(callback);
},
runRefresh,
});
scheduler.markNavigationStart();
scheduler.schedule();
scheduler.schedule();
expect(runRefresh).not.toHaveBeenCalled();
queued.shift()?.();
expect(runRefresh).not.toHaveBeenCalled();
scheduler.markNavigationSettled();
queued.shift()?.();
expect(runRefresh).toHaveBeenCalledTimes(1);
scheduler.schedule();
queued.shift()?.();
expect(runRefresh).toHaveBeenCalledTimes(2);
});
it("does not expose a per-navigation transition override at the controller boundary", () => {
type Controller = ReturnType<typeof createAppBrowserNavigationController>;
function assertNoTransitionOverride(controller: Controller) {
void renderCurrentStateNavigationPayload(controller, {
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/initial", {}),
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
operationLane: "navigation",
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: 1,
// @ts-expect-error ordinary navigations must not choose their own scheduling lane.
useTransition: false,
});
}
expect(assertNoTransitionOverride).toBeTypeOf("function");
});
it("restores visible history snapshots through an approved traversal commit", () => {
const currentState = createState({
elements: createResolvedElements("route:/scroll-restoration/other", "/"),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/scroll-restoration/other",
{},
),
routeId: "route:/scroll-restoration/other",
visibleCommitVersion: 7,
});
const snapshotState = createState({
elements: createResolvedElements("route:/scroll-restoration", "/"),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/scroll-restoration",
{},
),
routeId: "route:/scroll-restoration",
visibleCommitVersion: 2,
});
const { controller, stateRef } = createControllerHarness(currentState);
const navId = controller.beginNavigation();
const restored = controller.restoreHistorySnapshotVisibleState({
navId,
state: snapshotState,
targetHref: "https://example.com/scroll-restoration",
});
expect(restored).toBe(true);
expect(stateRef.current.routeId).toBe("route:/scroll-restoration");
expect(stateRef.current.visibleCommitVersion).toBe(8);
expect(stateRef.current.activeOperation).toMatchObject({
lane: "traverse",
startedVisibleCommitVersion: 7,
state: "committed",
visibleCommitVersion: 8,
});
});
it("matches visible history snapshot targets after stripping basePath and canonicalizing search", () => {
const currentState = createState({
elements: createResolvedElements("route:/scroll-restoration/other", "/"),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/scroll-restoration/other",
{},
),
routeId: "route:/scroll-restoration/other",
visibleCommitVersion: 7,
});
const snapshotState = createState({
elements: createResolvedElements("route:/scroll-restoration", "/"),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/scroll-restoration?q=a+b",
{},
),
routeId: "route:/scroll-restoration",
visibleCommitVersion: 2,
});
const { controller, stateRef } = createControllerHarness(currentState, {
basePath: "/docs",
});
const navId = controller.beginNavigation();
const restored = controller.restoreHistorySnapshotVisibleState({
navId,
state: snapshotState,
targetHref: "https://example.com/docs/scroll-restoration?q=a%20b",
});
expect(restored).toBe(true);
expect(stateRef.current.routeId).toBe("route:/scroll-restoration");
expect(stateRef.current.visibleCommitVersion).toBe(8);
});
it("rejects visible history snapshots for stale navigation lifecycles", () => {
const currentState = createState({ visibleCommitVersion: 7 });
const snapshotState = createState({
elements: createResolvedElements("route:/scroll-restoration", "/"),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/scroll-restoration",
{},
),
routeId: "route:/scroll-restoration",
});
const { controller, stateRef } = createControllerHarness(currentState);
const staleNavId = controller.beginNavigation();
controller.beginNavigation();
const restored = controller.restoreHistorySnapshotVisibleState({
navId: staleNavId,
state: snapshotState,
targetHref: "https://example.com/scroll-restoration",
});
expect(restored).toBe(false);
expect(stateRef.current).toBe(currentState);
});
it("rejects visible history snapshots when the active target does not match", () => {
const currentState = createState({ visibleCommitVersion: 7 });
const snapshotState = createState({
elements: createResolvedElements("route:/scroll-restoration", "/"),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/scroll-restoration",
{},
),
routeId: "route:/scroll-restoration",
});
const { controller, stateRef } = createControllerHarness(currentState);
const navId = controller.beginNavigation();
const restored = controller.restoreHistorySnapshotVisibleState({
navId,
state: snapshotState,
targetHref: "https://example.com/scroll-restoration/other",
});
expect(restored).toBe(false);
expect(stateRef.current).toBe(currentState);
});
});
describe("app browser entry state helpers", () => {
it("requires renderId when creating pending commits", () => {
// @ts-expect-error renderId is required to avoid duplicate commit ids.
void createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: createState(),
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
navigationSnapshot: createState().navigationSnapshot,
operationLane: "navigation",
type: "navigate",
});
});
it("merges elements on approved navigate commits", async () => {
const previousElements = createResolvedElements("route:/initial", "/", null, {
"layout:/": React.createElement("div", null, "layout"),
});
const state = createState({
bfcacheIds: {
"layout:/": "0",
},
elements: previousElements,
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/next": React.createElement("main", null, "next"),
},
rootLayoutTreePath: "/",
routeId: "route:/next",
});
expect(nextState.routeId).toBe("route:/next");
expect(nextState.interceptionContext).toBeNull();
expect(nextState.previousNextUrl).toBeNull();
expect(nextState.rootLayoutTreePath).toBe("/");
expect(nextState.visibleCommitVersion).toBe(1);
expect(nextState.activeOperation).toMatchObject({
id: 1,
lane: "navigation",
startedVisibleCommitVersion: 0,
state: "committed",
visibleCommitVersion: 1,
});
expect(nextState.elements).toMatchObject({
"layout:/": expect.anything(),
"page:/next": expect.anything(),
});
});
it("merges planner-approved elements on navigation replace commits", async () => {
const nextElements = createResolvedElements("route:/next", "/", null, {
"page:/next": React.createElement("main", null, "next"),
});
const state = createState();
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: state,
nextElements: Promise.resolve(nextElements),
navigationSnapshot: state.navigationSnapshot,
operationLane: "navigation",
renderId: 1,
type: "replace",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState: state,
pending,
routeManifest: createRouteManifestForPendingCommit(state, pending),
startedNavigationId: 1,
targetHref: "https://example.com/next",
});
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
const nextState = applyApprovedVisibleCommit(state, approval.approvedCommit);
expect(nextState.elements).toEqual(nextElements);
expect(nextState.interceptionContext).toBeNull();
expect(nextState.previousNextUrl).toBeNull();
expect(nextState.elements).toMatchObject({
"page:/next": expect.anything(),
});
});
it("increments the visible commit version once per approved visible commit", async () => {
const initialState = createState();
const firstState = await applyApprovedTestCommit(initialState, {
renderId: 101,
rootLayoutTreePath: "/",
routeId: "route:/one",
});
const secondState = await applyApprovedTestCommit(firstState, {
renderId: 102,
rootLayoutTreePath: "/",
routeId: "route:/two",
});
expect(firstState.visibleCommitVersion).toBe(1);
expect(secondState.visibleCommitVersion).toBe(2);
expect(secondState.activeOperation).toMatchObject({
id: 102,
startedVisibleCommitVersion: 1,
state: "committed",
visibleCommitVersion: 2,
});
});
it("does not export a raw visible state reducer outside the approved commit boundary", async () => {
const stateModule = await import("../packages/vinext/src/server/app-browser-state.js");
expect(Object.hasOwn(stateModule, "routerReducer")).toBe(false);
});
it("carries interception context through pending navigation commits", async () => {
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: createState(),
nextElements: Promise.resolve(
createResolvedElements(
"route:/feed",
"/",
"/feed",
{
"page:/feed": React.createElement("main", null, "feed"),
},
[AppElementsWire.encodeLayoutId("/")],
[],
createInterceptionProof("/feed", "/photos/42"),
),
),
navigationSnapshot: createState().navigationSnapshot,
operationLane: "navigation",
previousNextUrl: "/feed",
renderId: 1,
type: "navigate",
});
expect(pending.routeId).toBe("route:/feed");
expect(pending.interception).toEqual(createInterceptionProof("/feed", "/photos/42"));
expect(pending.interceptionContext).toBe("/feed");
expect(pending.previousNextUrl).toBe("/feed");
expect(pending.action.interception).toEqual(createInterceptionProof("/feed", "/photos/42"));
expect(pending.action.interceptionContext).toBe("/feed");
expect(pending.action.previousNextUrl).toBe("/feed");
});
it("mints fresh bfcache ids instead of reusing current ids when requested", async () => {
const currentRootLayout = React.createElement("div", null, "current root");
const nextRootLayout = React.createElement("div", null, "next root");
const currentState = createState({
bfcacheIds: {
"layout:/": "_b_4_",
},
elements: createResolvedElements("route:/dashboard", "/", null, {
"layout:/": currentRootLayout,
}),
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements("route:/settings", "/", null, {
"layout:/": nextRootLayout,
}),
),
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {}),
operationLane: "traverse",
renderId: 1,
reuseCurrentBfcacheIds: false,
type: "traverse",
});
expect(pending.action.reuseCurrentBfcacheIds).toBe(false);
expect(pending.action.bfcacheIds["layout:/"]).toMatch(/^_b_\d+_$/);
expect(pending.action.bfcacheIds["layout:/"]).not.toBe("_b_4_");
});
it("clears previousNextUrl when traversing to a non-intercepted entry", async () => {
// Traversing back from an intercepted modal (/photos/42 from /feed) to
// /feed itself. The traverse branch reads null from /feed's history state
// and passes previousNextUrl: null explicitly — meaning "not intercepted".
// This must not inherit the current state's stale "/feed" value.
const interceptedState = createState({
interceptionContext: "/feed",
previousNextUrl: "/feed",
routeId: "route:/feed",
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: interceptedState,
nextElements: Promise.resolve(createResolvedElements("route:/feed", "/")),
navigationSnapshot: createState().navigationSnapshot,
operationLane: "traverse",
previousNextUrl: null,
renderId: 2,
type: "traverse",
});
expect(pending.previousNextUrl).toBeNull();
expect(pending.action.previousNextUrl).toBeNull();
});
it("hard navigates instead of merging when the root layout changes", async () => {
const currentState = createState({
rootLayoutTreePath: "/(marketing)",
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/(dashboard)")),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 1,
type: "navigate",
});
expect(
resolvePendingNavigationCommitDispositionDecision({
activeNavigationId: 3,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 3,
}).disposition,
).toBe("hard-navigate");
});
it("defers commit classification until the payload has resolved", async () => {
let resolveElements: ((value: AppElements) => void) | undefined;
const nextElements = new Promise<AppElements>((resolve) => {
resolveElements = resolve;
});
let resolved = false;
const pending = createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: createState(),
nextElements,
navigationSnapshot: createState().navigationSnapshot,
operationLane: "navigation",
renderId: 1,
type: "navigate",
}).then((result) => {
resolved = true;
return result;
});
expect(resolved).toBe(false);
if (!resolveElements) {
throw new Error("Expected deferred elements resolver");
}
resolveElements(
normalizeAppElements({
[APP_ROUTE_KEY]: "route:/dashboard",
[APP_ROOT_LAYOUT_KEY]: "/",
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
const result = await pending;
expect(resolved).toBe(true);
expect(result.routeId).toBe("route:/dashboard");
});
it("creates pending operation records from the current visible commit version", async () => {
const currentState = createState({
visibleCommitVersion: 4,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "refresh",
renderId: 9,
type: "navigate",
});
expect(pending.action.operation).toEqual({
id: 9,
lane: "refresh",
startedVisibleCommitVersion: 4,
state: "pending",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 1,
targetHref: "https://example.com/dashboard",
});
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
const committedState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(committedState.visibleCommitVersion).toBe(5);
expect(committedState.activeOperation).toEqual({
id: 9,
lane: "refresh",
startedVisibleCommitVersion: 4,
state: "committed",
visibleCommitVersion: 5,
});
});
it("skips a pending commit when a newer navigation has become active", async () => {
const currentState = createState();
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 1,
type: "navigate",
});
expect(
resolvePendingNavigationCommitDispositionDecision({
activeNavigationId: 5,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 4,
}).disposition,
).toBe("skip");
});
it("skips a refresh commit when visible state changed after the refresh started", async () => {
const refreshStartState = createState({ visibleCommitVersion: 4 });
const latestState = createState({
routeId: "route:/hmr",
visibleCommitVersion: 5,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: refreshStartState,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
navigationSnapshot: refreshStartState.navigationSnapshot,
operationLane: "refresh",
renderId: 22,
type: "navigate",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 7,
currentState: latestState,
pending,
routeManifest: createRouteManifestForPendingCommit(latestState, pending),
startedNavigationId: 7,
targetHref: "https://example.com/dashboard",
});
expect(approval.decision.disposition).toBe("no-commit");
expect(approval.decision.trace.entries).toEqual([
{
code: NavigationTraceTransactionCodes.noCommit,
fields: {
operationLane: "refresh",
pendingOperationId: 22,
startedVisibleCommitVersion: 4,
},
},
{
code: NavigationTraceReasonCodes.staleOperation,
fields: {
activeNavigationId: 7,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 5,
nextRootLayoutTreePath: "/",
startedNavigationId: 7,
startedVisibleCommitVersion: 4,
targetHref: "https://example.com/dashboard",
},
},
]);
expect(approval.approvedCommit).toBeNull();
});
it("skips a traverse commit when visible state changed after traversal started", async () => {
const traverseStartState = createState({ visibleCommitVersion: 2 });
const latestState = createState({
routeId: "route:/newer",
visibleCommitVersion: 3,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: traverseStartState,
nextElements: Promise.resolve(createResolvedElements("route:/previous", "/")),
navigationSnapshot: traverseStartState.navigationSnapshot,
operationLane: "traverse",
previousNextUrl: null,
renderId: 23,
type: "traverse",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 8,
currentState: latestState,
pending,
routeManifest: createRouteManifestForPendingCommit(latestState, pending),
startedNavigationId: 8,
targetHref: "https://example.com/previous",
});
expect(approval.decision.disposition).toBe("no-commit");
expect(approval.decision.trace.entries).toEqual([
{
code: NavigationTraceTransactionCodes.noCommit,
fields: {
operationLane: "traverse",
pendingOperationId: 23,
startedVisibleCommitVersion: 2,
},
},
{
code: NavigationTraceReasonCodes.staleOperation,
fields: {
activeNavigationId: 8,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 3,
nextRootLayoutTreePath: "/",
startedNavigationId: 8,
startedVisibleCommitVersion: 2,
targetHref: "https://example.com/previous",
},
},
]);
expect(approval.approvedCommit).toBeNull();
});
it("traces stale pending commits with compact reason codes and structured fields", async () => {
const decision = await resolveTestPendingNavigationCommitDispositionDecision({
activeNavigationId: 5,
currentVisibleCommitVersion: 0,
currentRootLayoutTreePath: "/",
nextRootLayoutTreePath: "/(dashboard)",
startedNavigationId: 4,
startedVisibleCommitVersion: 0,
});
expect(decision.disposition).toBe("skip");
expect(decision.trace).toEqual({
schemaVersion: NAVIGATION_TRACE_SCHEMA_VERSION,
entries: [
{
code: NavigationTraceReasonCodes.staleOperation,
fields: {
activeNavigationId: 5,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 0,
nextRootLayoutTreePath: "/(dashboard)",
startedNavigationId: 4,
startedVisibleCommitVersion: 0,
},
},
],
});
});
it("treats a visible commit version mismatch as stale before root-boundary decisions", async () => {
const decision = await resolveTestPendingNavigationCommitDispositionDecision({
activeNavigationId: 2,
currentVisibleCommitVersion: 1,
currentRootLayoutTreePath: "/",
nextRootLayoutTreePath: "/",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
});
expect(decision.disposition).toBe("skip");
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.staleOperation);
});
it("treats stale state as authoritative even when the root boundary changed", async () => {
const decision = await resolveTestPendingNavigationCommitDispositionDecision({
activeNavigationId: 2,
currentVisibleCommitVersion: 1,
currentRootLayoutTreePath: "/(marketing)",
nextRootLayoutTreePath: "/(dashboard)",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
});
expect(decision).toEqual({
disposition: "skip",
preserveElementIds: [],
trace: {
schemaVersion: NAVIGATION_TRACE_SCHEMA_VERSION,
entries: [
{
code: NavigationTraceReasonCodes.staleOperation,
fields: {
activeNavigationId: 2,
currentRootLayoutTreePath: "/(marketing)",
currentVisibleCommitVersion: 1,
nextRootLayoutTreePath: "/(dashboard)",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
},
},
],
},
});
});
it("traces root-boundary hard navigation decisions", async () => {
const decision = await resolveTestPendingNavigationCommitDispositionDecision({
activeNavigationId: 2,
currentVisibleCommitVersion: 0,
currentRootLayoutTreePath: "/(marketing)",
nextRootLayoutTreePath: "/(dashboard)",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
});
expect(decision.disposition).toBe("hard-navigate");
expect(decision.trace.entries).toEqual([
{
code: NavigationTraceReasonCodes.rootBoundaryChanged,
fields: {
activeNavigationId: 2,
currentRootLayoutTreePath: "/(marketing)",
currentVisibleCommitVersion: 0,
nextRootLayoutTreePath: "/(dashboard)",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
},
},
]);
});
it("rejects cache-restored pending commits when cache-entry proof metadata is absent", async () => {
const currentState = createState({
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard/profile",
{},
),
routeId: "route:/dashboard/profile",
});
const pending = await createPendingNavigationCommit({
currentState,
nextElements: Promise.resolve(
createResolvedElements(
"route:/dashboard/settings",
"/",
null,
{
"page:/dashboard/settings": React.createElement("main", null, "settings"),
},
["layout:/"],
),
),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard/settings",
{},
),
operationLane: "navigation",
renderId: 2,
payloadOrigin: VISITED_CACHE_APP_NAVIGATION_PAYLOAD_ORIGIN,
type: "navigate",
});
const decision = resolvePendingNavigationCommitDispositionDecision({
activeNavigationId: 2,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 2,
targetHref: "https://example.com/dashboard/settings",
});
expect(decision.disposition).toBe("hard-navigate");
expect(decision.trace.entries).toEqual([
{
code: NavigationTraceReasonCodes.cacheProofRejected,
fields: {
activeNavigationId: 2,
cacheProofCode: "CP_CACHE_ENTRY_PROOF_MISSING",
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 0,
nextRootLayoutTreePath: "/",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
targetHref: "https://example.com/dashboard/settings",
},
},
]);
});
it("classifies complete dynamic payload metadata as client-cacheable", () => {
const elements = createResolvedElements("route:/dashboard/settings", "/", null, {
"page:/dashboard/settings": React.createElement("main", null, "settings"),
});
expect(isCompleteAppPayloadMetadata(AppElementsWire.readMetadata(elements))).toBe(true);
expect(isCacheRestorableAppPayloadMetadata(AppElementsWire.readMetadata(elements))).toBe(false);
});
it("does not classify skip-pruned payload metadata as cache-restorable", () => {
const layoutId = AppElementsWire.encodeLayoutId("/");
const elements = createResolvedElements(
"route:/dashboard/settings",
"/",
null,
{
[APP_CACHE_ENTRY_REUSE_PROOF_KEY]: createCacheEntryReuseProof(null),
[APP_SKIPPED_LAYOUT_IDS_KEY]: [layoutId],
"page:/dashboard/settings": React.createElement("main", null, "settings"),
},
[layoutId],
);
expect(isCompleteAppPayloadMetadata(AppElementsWire.readMetadata(elements))).toBe(false);
});
it("traces unknown root-layout identity without preserving absent slots", async () => {
const decision = await resolveTestPendingNavigationCommitDispositionDecision({
activeNavigationId: 2,
currentVisibleCommitVersion: 0,
currentRootLayoutTreePath: "/",
nextRootLayoutTreePath: null,
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
});
expect(decision.disposition).toBe("dispatch");
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.rootBoundaryUnknown);
});
it("traces matching root-layout dispatches as current commits", async () => {
const decision = await resolveTestPendingNavigationCommitDispositionDecision({
activeNavigationId: 2,
currentVisibleCommitVersion: 0,
currentRootLayoutTreePath: "/",
nextRootLayoutTreePath: "/",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
});
expect(decision.disposition).toBe("dispatch");
expect(decision.trace.entries).toEqual([
{
code: NavigationTraceReasonCodes.commitCurrent,
fields: {
activeNavigationId: 2,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 0,
nextRootLayoutTreePath: "/",
startedNavigationId: 2,
startedVisibleCommitVersion: 0,
},
},
]);
});
it("normalizes encoded route-state paths before validating interception proof", async () => {
const slotId = AppElementsWire.encodeSlotId("modal", "/café");
const currentState = createState({
layoutIds: [AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/café")],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/caf%C3%A9", {}),
rootLayoutTreePath: "/",
routeId: AppElementsWire.encodeRouteId("/café", null),
slotBindings: [
{
ownerLayoutId: AppElementsWire.encodeLayoutId("/café"),
slotId,
state: "default",
},
],
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements(
AppElementsWire.encodeRouteId("/café", null),
"/",
"/caf%C3%A9",
{
"page:/café": React.createElement("main", null, "source"),
},
[AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/café")],
[
{
ownerLayoutId: AppElementsWire.encodeLayoutId("/café"),
slotId,
state: "active",
},
],
createInterceptionProof("/café", "/photos/café", slotId),
),
),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/photos/caf%C3%A9",
{},
),
operationLane: "navigation",
previousNextUrl: "/caf%C3%A9",
renderId: 1,
type: "navigate",
});
const decision = resolvePendingNavigationCommitDispositionDecision({
activeNavigationId: 1,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 1,
});
expect(decision.disposition).toBe("dispatch");
if (decision.disposition !== "dispatch") {
throw new Error("Expected dispatch decision");
}
expect(decision.preserveElementIds).toEqual([
AppElementsWire.encodeLayoutId("/"),
AppElementsWire.encodeLayoutId("/café"),
]);
expect(decision.trace.entries[0]?.code).toBe(
NavigationTraceReasonCodes.interceptedCommitCurrent,
);
});
it("builds a merge commit for refresh and server-action payloads", async () => {
const refreshCommit = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: createState(),
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
navigationSnapshot: createState().navigationSnapshot,
operationLane: "refresh",
previousNextUrl: "/feed",
renderId: 1,
type: "navigate",
});
expect(refreshCommit.action.type).toBe("navigate");
expect(refreshCommit.routeId).toBe("route:/dashboard");
expect(refreshCommit.rootLayoutTreePath).toBe("/");
expect(refreshCommit.previousNextUrl).toBeNull();
});
it("commits non-intercepted context-only payloads without preserving stale interception state", async () => {
const currentState = createState({
interception: createInterceptionProof("/feed", "/photos/42"),
interceptionContext: "/feed",
layoutIds: [AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/feed")],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/photos/42", {}),
previousNextUrl: "/feed",
rootLayoutTreePath: "/",
routeId: AppElementsWire.encodeRouteId("/feed", null),
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements(
AppElementsWire.encodeRouteId("/feed", null),
"/",
"/feed",
{
[AppElementsWire.encodePageId("/feed", null)]: React.createElement(
"main",
null,
"feed",
),
},
[AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/feed")],
),
),
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/feed", {}),
operationLane: "refresh",
previousNextUrl: "/feed",
renderId: 1,
type: "navigate",
});
const decision = resolvePendingNavigationCommitDispositionDecision({
activeNavigationId: 1,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 1,
});
expect(pending.interception).toBeNull();
expect(pending.interceptionContext).toBe("/feed");
expect(pending.previousNextUrl).toBeNull();
expect(decision.disposition).toBe("dispatch");
expect(decision.trace.entries[0]?.code).toBe(NavigationTraceReasonCodes.commitCurrent);
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 1,
targetHref: "https://example.com/feed",
});
if (approval.approvedCommit === null) {
throw new Error("Expected context-only payload to commit");
}
const contextOnlyState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(contextOnlyState.interception).toBeNull();
expect(contextOnlyState.previousNextUrl).toBeNull();
expect(contextOnlyState.routeId).toBe(AppElementsWire.encodeRouteId("/feed", null));
const interceptedPending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: contextOnlyState,
nextElements: Promise.resolve(
createResolvedElements(
AppElementsWire.encodeRouteId("/feed", null),
"/",
"/feed",
{
[AppElementsWire.encodePageId("/feed", null)]: React.createElement(
"main",
null,
"feed",
),
[AppElementsWire.encodeSlotId("modal", "/feed")]: React.createElement(
"div",
null,
"modal",
),
},
[AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/feed")],
[
{
ownerLayoutId: AppElementsWire.encodeLayoutId("/feed"),
slotId: AppElementsWire.encodeSlotId("modal", "/feed"),
state: "active",
},
],
createInterceptionProof("/feed", "/photos/42"),
),
),
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/photos/42", {}),
operationLane: "navigation",
previousNextUrl: "/feed",
renderId: 2,
type: "navigate",
});
const interceptedApproval = approvePendingNavigationCommit({
activeNavigationId: 2,
currentState: contextOnlyState,
pending: interceptedPending,
routeManifest: createRouteManifestForPendingCommit(contextOnlyState, interceptedPending),
startedNavigationId: 2,
targetHref: "https://example.com/photos/42",
});
expect(interceptedApproval.decision.disposition).toBe("commit");
expect(interceptedApproval.decision.trace.entries[1]?.code).toBe(
NavigationTraceReasonCodes.interceptedCommitCurrent,
);
});
it("creates an approved visible commit only after the current operation decision allows mutation", async () => {
const currentState = createState();
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 11,
type: "navigate",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 4,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 4,
targetHref: "https://example.com/dashboard",
});
expect(approval.decision.disposition).toBe("commit");
if (approval.decision.disposition !== "commit") {
throw new Error("Expected visible commit approval");
}
expect(approval.decision.preserveAbsentSlots).toBe(false);
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
expect(approval.decision.trace.entries).toEqual([
{
code: NavigationTraceTransactionCodes.visibleCommit,
fields: {
operationLane: "navigation",
pendingOperationId: 11,
startedVisibleCommitVersion: 0,
},
},
{
code: NavigationTraceReasonCodes.commitCurrent,
fields: {
activeNavigationId: 4,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 0,
nextRootLayoutTreePath: "/",
startedNavigationId: 4,
startedVisibleCommitVersion: 0,
targetHref: "https://example.com/dashboard",
},
},
]);
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(nextState.routeId).toBe("route:/dashboard");
expect(nextState.visibleCommitVersion).toBe(1);
expect(nextState.activeOperation).toMatchObject({
id: 11,
lane: "navigation",
startedVisibleCommitVersion: 0,
state: "committed",
visibleCommitVersion: 1,
});
});
it("traces unknown root-layout approval as an unproven payload commit", async () => {
const currentState = createState();
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/legacy-payload", null)),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 16,
type: "navigate",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 6,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 6,
targetHref: "https://example.com/legacy-payload",
});
expect(approval.decision.disposition).toBe("commit");
if (approval.decision.disposition !== "commit") {
throw new Error("Expected visible commit approval");
}
expect(approval.decision.preserveAbsentSlots).toBe(false);
expect(approval.decision.trace.entries[0]?.code).toBe(
NavigationTraceTransactionCodes.visibleCommit,
);
expect(approval.decision.trace.entries[1]?.code).toBe(
NavigationTraceReasonCodes.rootBoundaryUnknown,
);
expect(approval.approvedCommit).not.toBeNull();
});
it("approves HMR visible commits through a named trusted recovery path", async () => {
const currentState = createState();
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements("route:/hmr", "/", null, {
"page:/hmr": React.createElement("main", null, "hmr"),
}),
),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "hmr",
renderId: 14,
type: "replace",
});
const approval = approveHmrVisibleCommit({
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
targetHref: "https://example.com/hmr",
});
const approvedCommit = approval.approvedCommit;
expect(approvedCommit).not.toBeNull();
if (!approvedCommit) throw new Error("Expected HMR visible commit approval");
expect(approvedCommit.decision.trace.entries[0]).toEqual({
code: NavigationTraceTransactionCodes.visibleCommit,
fields: {
operationLane: "hmr",
pendingOperationId: 14,
startedVisibleCommitVersion: 0,
},
});
const nextState = applyApprovedVisibleCommit(currentState, approvedCommit);
expect(nextState.routeId).toBe("route:/hmr");
expect(nextState.activeOperation).toMatchObject({
id: 14,
lane: "hmr",
state: "committed",
});
});
it("rejects non-HMR commits on the HMR approval path", async () => {
const currentState = createState();
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 15,
type: "replace",
});
expect(() =>
approveHmrVisibleCommit({
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
targetHref: "https://example.com/dashboard",
}),
).toThrow("[vinext] HMR visible commit approval requires an HMR pending operation");
});
it("refreshes planner-approved layout elements across HMR replacement", async () => {
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const routeId = AppElementsWire.encodeRouteId("/dev-overlay-layout-hmr-toggle", null);
const currentRootLayout = React.createElement("section", null, "layout hmr clean");
const nextRootLayout = React.createElement("section", null, "layout hmr throw");
const currentRouteShell = React.createElement("div", null, "current route shell");
const nextRouteShell = React.createElement("div", null, "next route shell");
const currentState = createState({
bfcacheIds: {
[rootLayoutId]: "0",
},
elements: createResolvedElements(
routeId,
"/",
null,
{
[rootLayoutId]: currentRootLayout,
[routeId]: currentRouteShell,
"page:/dev-overlay-layout-hmr-toggle": React.createElement("main", null, "page"),
},
[rootLayoutId],
),
layoutIds: [rootLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dev-overlay-layout-hmr-toggle",
{},
),
routeId,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements(
routeId,
"/",
null,
{
[rootLayoutId]: nextRootLayout,
[routeId]: nextRouteShell,
"page:/dev-overlay-layout-hmr-toggle": React.createElement("main", null, "page"),
},
[rootLayoutId],
),
),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "hmr",
renderId: 15,
type: "replace",
});
const approval = approveHmrVisibleCommit({
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
targetHref: "https://example.com/dev-overlay-layout-hmr-toggle",
});
const approvedCommit = approval.approvedCommit;
expect(approvedCommit).not.toBeNull();
if (!approvedCommit) throw new Error("Expected HMR visible commit approval");
expect(approval.decision.preserveElementIds).toEqual([rootLayoutId]);
const nextState = applyApprovedVisibleCommit(currentState, approvedCommit);
expect(nextState.elements[routeId]).toBe(currentRouteShell);
expect(nextState.elements[rootLayoutId]).toBe(nextRootLayout);
});
it("preserves planner-approved named slot state across HMR replacement", async () => {
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const authSlotId = AppElementsWire.encodeSlotId("auth", "/");
const currentRootLayout = React.createElement("section", null, "current root layout");
const nextRootLayout = React.createElement("section", null, "next root layout");
const currentSlot = React.createElement("aside", null, "reset");
const currentBindings = [
{
ownerLayoutId: rootLayoutId,
slotId: authSlotId,
slotName: "auth",
state: "active" as const,
},
];
const targetBindings = [
{
ownerLayoutId: rootLayoutId,
slotId: authSlotId,
slotName: "auth",
state: "default" as const,
},
];
const currentState = createState({
bfcacheIds: {
[rootLayoutId]: "0",
},
elements: createResolvedElements(
"route:/parallel-selected-segment/foo",
"/",
null,
{ [authSlotId]: currentSlot, [rootLayoutId]: currentRootLayout },
[rootLayoutId],
currentBindings,
),
layoutIds: [rootLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/parallel-selected-segment/foo",
{},
),
routeId: "route:/parallel-selected-segment/foo",
slotBindings: currentBindings,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements(
"route:/parallel-selected-segment/foo",
"/",
null,
{
[authSlotId]: React.createElement("aside", null, "default"),
[rootLayoutId]: nextRootLayout,
},
[rootLayoutId],
targetBindings,
),
),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "hmr",
renderId: 15,
type: "replace",
});
const approval = approveHmrVisibleCommit({
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
targetHref: "https://example.com/parallel-selected-segment/foo",
});
const approvedCommit = approval.approvedCommit;
expect(approvedCommit).not.toBeNull();
if (!approvedCommit) throw new Error("Expected HMR visible commit approval");
expect(approvedCommit.decision.preservePreviousSlotIds).toEqual([authSlotId]);
const nextState = applyApprovedVisibleCommit(currentState, approvedCommit);
expect(nextState.elements[rootLayoutId]).toBe(currentRootLayout);
expect(nextState.elements[authSlotId]).toBe(currentSlot);
expect(nextState.slotBindings).toEqual(currentBindings);
});
it("does not let a pending HMR replacement overwrite a navigation commit", async () => {
const { controller, stateRef, setBrowserRouterState } = createControllerHarness();
let resolveHmrPayload!: (elements: AppElements) => void;
const hmrPayload = new Promise<AppElements>((resolve) => {
resolveHmrPayload = resolve;
});
const hmrPromise = controller.hmrReplaceTree(
hmrPayload,
createClientNavigationRenderSnapshot("https://example.com/hmr", {}),
);
const navigationState = await applyApprovedTestCommit(stateRef.current, {
activeNavigationId: 17,
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/navigation",
{},
),
renderId: 17,
rootLayoutTreePath: "/",
routeId: "route:/navigation",
startedNavigationId: 17,
targetHref: "https://example.com/navigation",
});
stateRef.current = navigationState;
resolveHmrPayload(createResolvedElements("route:/hmr", "/"));
await hmrPromise;
expect(stateRef.current.routeId).toBe("route:/navigation");
expect(stateRef.current.visibleCommitVersion).toBe(1);
expect(setBrowserRouterState).not.toHaveBeenCalled();
});
it("does not let faster HMR reject a navigation that started while HMR was pending", async () => {
const { controller, detach, stateRef } = createControllerHarness();
let resolveHmrPayload!: (elements: AppElements) => void;
let resolveNavigationPayload!: (elements: AppElements) => void;
const hmrPayload = new Promise<AppElements>((resolve) => {
resolveHmrPayload = resolve;
});
const navigationPayload = new Promise<AppElements>((resolve) => {
resolveNavigationPayload = resolve;
});
try {
const hmrPromise = controller.hmrReplaceTree(
hmrPayload,
createClientNavigationRenderSnapshot("https://example.com/initial", {}),
);
const navId = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/navigation",
{},
),
nextElements: navigationPayload,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/navigation",
navId,
});
resolveHmrPayload(createResolvedElements("route:/hmr", "/"));
await hmrPromise;
expect(stateRef.current.routeId).toBe("route:/initial");
resolveNavigationPayload(createResolvedElements("route:/navigation", "/"));
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/navigation");
expect(stateRef.current.activeOperation).toMatchObject({
lane: "navigation",
state: "committed",
});
} finally {
detach();
}
});
it("does not let HMR that starts during a pending navigation supersede it", async () => {
const { controller, detach, stateRef, setBrowserRouterState } = createControllerHarness();
let resolveNavigationPayload!: (elements: AppElements) => void;
const navigationPayload = new Promise<AppElements>((resolve) => {
resolveNavigationPayload = resolve;
});
try {
const navId = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/navigation",
{},
),
nextElements: navigationPayload,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/navigation",
navId,
});
await controller.hmrReplaceTree(
Promise.resolve(createResolvedElements("route:/hmr", "/")),
createClientNavigationRenderSnapshot("https://example.com/initial", {}),
);
expect(stateRef.current.routeId).toBe("route:/initial");
expect(setBrowserRouterState).not.toHaveBeenCalled();
resolveNavigationPayload(createResolvedElements("route:/navigation", "/"));
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/navigation");
expect(stateRef.current.activeOperation).toMatchObject({
lane: "navigation",
state: "committed",
});
} finally {
detach();
}
});
it("hard-navigates when HMR changes the root layout boundary", async () => {
const performHardNavigation = vi.fn(() => true);
const currentState = createState({
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/marketing", {}),
rootLayoutTreePath: "/(marketing)",
routeId: "route:/marketing",
});
const { controller, detach, stateRef, setBrowserRouterState } = createControllerHarness(
currentState,
{
getRouteManifest: () =>
createTestRouteManifest([
{
id: "route:/marketing",
layoutIds: [AppElementsWire.encodeLayoutId("/(marketing)")],
pattern: "/marketing",
rootBoundaryId: "root-boundary:/(marketing)",
},
{
id: "route:/marketing-hmr",
layoutIds: [AppElementsWire.encodeLayoutId("/(shop)")],
pattern: "/marketing",
rootBoundaryId: "root-boundary:/(shop)",
},
]),
performHardNavigation,
},
);
try {
await controller.hmrReplaceTree(
Promise.resolve(createResolvedElements("route:/marketing-hmr", "/(shop)")),
currentState.navigationSnapshot,
);
expect(performHardNavigation).toHaveBeenCalledWith("/marketing");
expect(setBrowserRouterState).not.toHaveBeenCalled();
expect(stateRef.current).toBe(currentState);
} finally {
detach();
}
});
it("does not commit an older HMR payload after a newer HMR update starts", async () => {
const { controller, detach, stateRef, setBrowserRouterState } = createControllerHarness();
let resolveFirstHmrPayload!: (elements: AppElements) => void;
let resolveSecondHmrPayload!: (elements: AppElements) => void;
const firstHmrPayload = new Promise<AppElements>((resolve) => {
resolveFirstHmrPayload = resolve;
});
const secondHmrPayload = new Promise<AppElements>((resolve) => {
resolveSecondHmrPayload = resolve;
});
try {
const firstHmrPromise = controller.hmrReplaceTree(
firstHmrPayload,
stateRef.current.navigationSnapshot,
);
const secondHmrPromise = controller.hmrReplaceTree(
secondHmrPayload,
stateRef.current.navigationSnapshot,
);
resolveFirstHmrPayload(createResolvedElements("route:/hmr-a", "/"));
await firstHmrPromise;
expect(stateRef.current.routeId).toBe("route:/initial");
expect(setBrowserRouterState).not.toHaveBeenCalled();
resolveSecondHmrPayload(createResolvedElements("route:/hmr-b", "/"));
await secondHmrPromise;
expect(stateRef.current.routeId).toBe("route:/hmr-b");
expect(setBrowserRouterState).toHaveBeenCalledTimes(1);
} finally {
detach();
}
});
it("does not preserve unapproved old elements on navigation replace commits", async () => {
const currentState = createState({
elements: createResolvedElements("route:/initial", "/", null, {
"layout:/old": React.createElement("div", null, "old"),
}),
});
const nextElements = createResolvedElements("route:/next", "/", null, {
"page:/next": React.createElement("main", null, "next"),
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(nextElements),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 12,
type: "replace",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 4,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 4,
targetHref: "https://example.com/next",
});
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(nextState.elements).toEqual(nextElements);
expect(Object.hasOwn(nextState.elements, "layout:/old")).toBe(false);
expect(nextState.activeOperation).toMatchObject({
id: 12,
lane: "navigation",
state: "committed",
});
});
it("replace navigation preserves planner-approved default-only slot content", async () => {
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const authSlotId = AppElementsWire.encodeSlotId("auth", "/");
const currentSlot = React.createElement("aside", null, "reset");
const currentBindings = [
{
ownerLayoutId: rootLayoutId,
slotId: authSlotId,
slotName: "auth",
state: "active" as const,
},
];
const targetBindings = [
{
ownerLayoutId: rootLayoutId,
slotId: authSlotId,
slotName: "auth",
state: "default" as const,
},
];
const currentState = createState({
bfcacheIds: {
[rootLayoutId]: "0",
},
elements: createResolvedElements(
"route:/parallel-selected-segment/reset",
"/",
null,
{ [authSlotId]: currentSlot },
[rootLayoutId],
currentBindings,
),
layoutIds: [rootLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/parallel-selected-segment/reset",
{},
),
routeId: "route:/parallel-selected-segment/reset",
slotBindings: currentBindings,
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createResolvedElements(
"route:/parallel-selected-segment/foo",
"/",
null,
{ "page:/parallel-selected-segment/foo": React.createElement("main", null, "foo") },
[rootLayoutId],
targetBindings,
),
),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/parallel-selected-segment/foo",
{},
),
operationLane: "navigation",
renderId: 13,
type: "replace",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 5,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 5,
targetHref: "https://example.com/parallel-selected-segment/foo",
});
expect(approval.decision.disposition).toBe("commit");
if (approval.decision.disposition !== "commit" || approval.approvedCommit === null) {
throw new Error("Expected visible commit approval");
}
expect(approval.decision.preservePreviousSlotIds).toEqual([authSlotId]);
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(nextState.elements[authSlotId]).toBe(currentSlot);
expect(nextState.slotBindings).toEqual(currentBindings);
});
it("applies approved traverse commits with stale slot cleanup", async () => {
const currentState = createState({
elements: createResolvedElements("route:/feed/comments", "/", null, {
"slot:modal:/feed": React.createElement("div", null, "modal"),
}),
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/feed", "/")),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "traverse",
previousNextUrl: null,
renderId: 13,
type: "traverse",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 4,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 4,
targetHref: "https://example.com/feed",
});
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(nextState.routeId).toBe("route:/feed");
expect(Object.hasOwn(nextState.elements, "slot:modal:/feed")).toBe(false);
expect(nextState.activeOperation).toMatchObject({
id: 13,
lane: "traverse",
state: "committed",
});
});
it("does not create approved visible commits for stale or hard-navigation decisions", async () => {
const currentState = createState({
rootLayoutTreePath: "/(marketing)",
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/(dashboard)")),
navigationSnapshot: currentState.navigationSnapshot,
operationLane: "navigation",
renderId: 12,
type: "navigate",
});
const staleApproval = approvePendingNavigationCommit({
activeNavigationId: 8,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 7,
targetHref: "https://example.com/dashboard",
});
expect(staleApproval.decision.disposition).toBe("no-commit");
expect(staleApproval.decision.trace.entries[0]?.code).toBe(
NavigationTraceTransactionCodes.noCommit,
);
expect(staleApproval.decision.trace.entries[1]?.code).toBe(
NavigationTraceReasonCodes.staleOperation,
);
expect(staleApproval.approvedCommit).toBeNull();
const hardNavigateApproval = approvePendingNavigationCommit({
activeNavigationId: 8,
currentState,
pending,
routeManifest: createRouteManifestForPendingCommit(currentState, pending),
startedNavigationId: 8,
targetHref: "https://example.com/dashboard?from=planner",
});
expect(hardNavigateApproval.decision.disposition).toBe("hard-navigate");
expect(hardNavigateApproval.decision.trace.entries[0]?.code).toBe(
NavigationTraceTransactionCodes.hardNavigate,
);
expect(hardNavigateApproval.decision.trace.entries[1]?.code).toBe(
NavigationTraceReasonCodes.rootBoundaryChanged,
);
expect(hardNavigateApproval.decision.trace.entries[1]?.fields.targetHref).toBe(
"https://example.com/dashboard?from=planner",
);
expect(hardNavigateApproval.approvedCommit).toBeNull();
});
it("preserves layoutFlags only for approved same-layout ancestors", async () => {
const state = createState({
bfcacheIds: { "layout:/": "0" },
layoutFlags: { "layout:/": "s", "layout:/old": "d" },
});
const nextState = await applyApprovedTestCommit(state, {
layoutFlags: { "layout:/blog": "d" },
layoutIds: ["layout:/", "layout:/blog"],
rootLayoutTreePath: "/",
routeId: "route:/next",
});
expect(nextState.layoutFlags).toEqual({
"layout:/": "s",
"layout:/blog": "d",
});
});
it("replaces layoutFlags on approved replace commits", async () => {
const state = createState({ layoutFlags: { "layout:/": "s", "layout:/old": "d" } });
const nextState = await applyApprovedTestCommit(state, {
layoutFlags: { "layout:/": "d" },
rootLayoutTreePath: "/",
routeId: "route:/next",
type: "replace",
});
// Replace: only new flags
expect(nextState.layoutFlags).toEqual({ "layout:/": "d" });
});
it("stores previousNextUrl on approved navigate commits", async () => {
const state = createState({
layoutIds: [AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/feed")],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/feed", {}),
routeId: "route:/feed",
slotBindings: [
{
ownerLayoutId: AppElementsWire.encodeLayoutId("/feed"),
slotId: AppElementsWire.encodeSlotId("modal", "/feed"),
state: "default",
},
],
});
const interception = createInterceptionProof("/feed", "/photos/42");
const nextState = await applyApprovedTestCommit(state, {
interception,
interceptionContext: "/feed",
layoutIds: [AppElementsWire.encodeLayoutId("/"), AppElementsWire.encodeLayoutId("/feed")],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/photos/42", {}),
previousNextUrl: "/feed",
rootLayoutTreePath: "/",
routeId: "route:/feed",
slotBindings: [
{
ownerLayoutId: AppElementsWire.encodeLayoutId("/feed"),
slotId: AppElementsWire.encodeSlotId("modal", "/feed"),
state: "active",
},
],
});
expect(nextState.interception).toEqual(interception);
expect(nextState.interceptionContext).toBe("/feed");
expect(nextState.previousNextUrl).toBe("/feed");
});
});
describe("app browser navigation controller", () => {
it("tracks active navigation ids and clears the pending pathname only for the current navigation", () => {
const { controller, detach } = createControllerHarness();
const clearSpy = vi.spyOn(navigationShim, "clearPendingPathname").mockImplementation(() => {});
try {
const firstNavId = controller.beginNavigation();
const secondNavId = controller.beginNavigation();
expect(controller.isCurrentNavigation(firstNavId)).toBe(false);
expect(controller.isCurrentNavigation(secondNavId)).toBe(true);
controller.finalizeNavigation(firstNavId, null);
expect(clearSpy).not.toHaveBeenCalled();
controller.finalizeNavigation(secondNavId, null);
expect(clearSpy).toHaveBeenCalledTimes(1);
expect(clearSpy).toHaveBeenCalledWith(secondNavId);
} finally {
detach();
}
});
it("uses render ids independent from navigation ids", async () => {
const { controller, detach, stateRef } = createControllerHarness();
const clearSpy = vi.spyOn(navigationShim, "clearPendingPathname").mockImplementation(() => {});
try {
// Navigation counter advances independently from render-id counter.
controller.beginNavigation(); // 1
controller.beginNavigation(); // 2
const navId = controller.beginNavigation(); // 3
const nextElements = Promise.resolve(
createResolvedElements("route:/dashboard", "/", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId,
});
// Yield microticks so the async function reaches dispatch and sets state.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
// renderId is 1 (first render allocation), independent from navId = 3.
expect(stateRef.current.renderId).toBe(1);
expect(stateRef.current.routeId).toBe("route:/dashboard");
} finally {
clearSpy.mockRestore();
detach();
}
});
it("reads RouteManifest lazily after generated browser globals are assigned", async () => {
let routeManifest: RouteManifest | null = null;
const performHardNavigation = vi.fn(() => true);
const { controller, detach, stateRef } = createControllerHarness(
createState({
layoutIds: ["layout:/stale"],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/app", {}),
rootLayoutTreePath: "/",
routeId: "route:/app",
}),
{
getRouteManifest: () => routeManifest,
performHardNavigation,
},
);
try {
const pendingRouterState = controller.beginPendingBrowserRouterState();
const navId = controller.beginNavigation();
let resolvePayload!: (elements: AppElements) => void;
const nextElements = new Promise<AppElements>((resolve) => {
resolvePayload = resolve;
});
const result = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/marketing",
{},
),
nextElements,
operationLane: "navigation",
params: {},
pendingRouterState,
previousNextUrl: null,
targetHref: "https://example.com/marketing",
navId,
});
routeManifest = createTestRouteManifest([
{
layoutIds: ["layout:/app"],
pattern: "/app",
rootBoundaryId: "root-boundary:/app",
},
{
layoutIds: ["layout:/marketing"],
pattern: "/marketing",
rootBoundaryId: "root-boundary:/marketing",
},
]);
resolvePayload(createResolvedElements("route:/marketing", "/", null, {}, ["layout:/stale"]));
await expect(result).resolves.toBe("hard-navigate");
await expect(pendingRouterState.promise).resolves.toBe(stateRef.current);
expect(performHardNavigation).toHaveBeenCalledWith("https://example.com/marketing");
expect(stateRef.current.routeId).toBe("route:/app");
} finally {
detach();
}
});
it("settles the previous pending browser-router promise when a newer pending state begins", async () => {
const { controller, detach, stateRef } = createControllerHarness();
const clearSpy = vi.spyOn(navigationShim, "clearPendingPathname").mockImplementation(() => {});
try {
const firstPending = controller.beginPendingBrowserRouterState();
expect(firstPending.settled).toBe(false);
const secondPending = controller.beginPendingBrowserRouterState();
await expect(firstPending.promise).resolves.toBe(stateRef.current);
expect(firstPending.settled).toBe(true);
controller.finalizeNavigation(controller.beginNavigation(), secondPending);
await expect(secondPending.promise).resolves.toBe(stateRef.current);
expect(secondPending.settled).toBe(true);
} finally {
clearSpy.mockRestore();
detach();
}
});
it("queues pre-paint commit effects and resolves the pending browser-router state on dispatch", async () => {
const { controller, detach, setBrowserRouterState, stateRef } = createControllerHarness();
const pendingRouterState = controller.beginPendingBrowserRouterState();
const commitEffect = vi.fn();
const createNavigationCommitEffect = vi.fn(() => commitEffect);
const nextElements = Promise.resolve(
createResolvedElements("route:/dashboard", "/", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
try {
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect,
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements,
operationLane: "navigation",
params: {},
pendingRouterState,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: controller.beginNavigation(),
});
await expect(pendingRouterState.promise).resolves.toMatchObject({
renderId: 1,
routeId: "route:/dashboard",
});
expect(createNavigationCommitEffect).toHaveBeenCalledTimes(1);
expect(commitEffect).not.toHaveBeenCalled();
expect(setBrowserRouterState).toHaveBeenCalledTimes(1);
} finally {
detach();
}
});
it("hard-navigates cache-restored payloads missing cache-entry proof metadata", async () => {
const performHardNavigation = vi.fn(() => true);
const createNavigationCommitEffect = vi.fn(() => vi.fn());
const currentState = createState({
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard/profile",
{},
),
routeId: "route:/dashboard/profile",
});
const routeManifest = createTestRouteManifest([
{
id: "route:/dashboard/profile",
layoutIds: currentState.layoutIds,
pattern: "/dashboard/profile",
rootBoundaryId: "root-boundary:/",
},
{
id: "route:/dashboard/settings",
layoutIds: [AppElementsWire.encodeLayoutId("/")],
pattern: "/dashboard/settings",
rootBoundaryId: "root-boundary:/",
},
]);
const { controller, detach, stateRef } = createControllerHarness(currentState, {
getRouteManifest: () => routeManifest,
performHardNavigation,
});
try {
const result = await renderCurrentStateNavigationPayload(controller, {
actionType: "navigate",
createNavigationCommitEffect,
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard/settings",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard/settings", "/", null, {
"page:/dashboard/settings": React.createElement("main", null, "settings"),
}),
),
operationLane: "navigation",
payloadOrigin: VISITED_CACHE_APP_NAVIGATION_PAYLOAD_ORIGIN,
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard/settings",
navId: controller.beginNavigation(),
});
expect(result).toBe("hard-navigate");
expect(performHardNavigation).toHaveBeenCalledWith("https://example.com/dashboard/settings");
expect(createNavigationCommitEffect).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/dashboard/profile");
} finally {
detach();
}
});
it("skips stale browser navigations before committing their payload", async () => {
const { controller, detach } = createControllerHarness();
const { assign } = stubWindow("https://example.com/initial");
vi.stubEnv("__NEXT_APP_NAV_FAIL_HANDLING", "true");
const createNavigationCommitEffect = vi.fn(() => vi.fn());
let resolveNextElements: ((value: AppElements) => void) | undefined;
const nextElements = new Promise<AppElements>((resolve) => {
resolveNextElements = resolve;
});
try {
const navId = controller.beginNavigation();
stageAppNavigationFailureTarget("/dashboard");
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect,
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/initial", {}),
nextElements,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId,
});
controller.beginNavigation();
if (!resolveNextElements) {
throw new Error("Expected deferred navigation payload resolver");
}
resolveNextElements(
createResolvedElements("route:/dashboard", "/", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
await expect(renderPromise).resolves.toBe("no-commit");
expect(createNavigationCommitEffect).not.toHaveBeenCalled();
expect(assign).not.toHaveBeenCalled();
expect(window.next?.__pendingUrl).toBeUndefined();
} finally {
detach();
vi.unstubAllEnvs();
}
});
it("forces same-path search and hash state into a synchronous React commit", async () => {
const initialElements = createResolvedElements("route:/hash", "/", null, {
"page:/hash": React.createElement("main", null, "hash page"),
});
const initialState = createState({
elements: initialElements,
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/hash#non-existent",
{},
),
routeId: "route:/hash",
});
const { controller, detach, setBrowserRouterState, stateRef } =
createControllerHarness(initialState);
const commitEffect = vi.fn();
const scrollIntent = beginAppRouterScrollIntent("#hash-160");
stubWindow("https://example.com/hash#non-existent");
try {
const navId = controller.beginNavigation();
const pendingRouterState = controller.beginPendingBrowserRouterState();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => commitEffect,
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/hash?with-query-param#hash-160",
{},
),
nextElements: Promise.resolve(initialElements),
operationLane: "navigation",
params: {},
pendingRouterState,
previousNextUrl: null,
scrollIntent,
targetHref: "https://example.com/hash?with-query-param#hash-160",
navId,
});
const outcome = await Promise.race([
renderPromise,
new Promise<"timeout">((resolve) => {
setTimeout(() => resolve("timeout"), 50);
}),
]);
expect(outcome).toBe("timeout");
expect(stateRef.current.renderId).toBeGreaterThan(0);
expect(stateRef.current.navigationSnapshot.searchParams.get("with-query-param")).toBe("");
expect(setBrowserRouterState).toHaveBeenLastCalledWith(stateRef.current);
// URL and scroll effects still wait for NavigationCommitSignal's layout
// effect; hash-rsc-requests.browser.spec.ts verifies that real commit.
expect(commitEffect).not.toHaveBeenCalled();
} finally {
clearAppRouterScrollIntent();
detach();
}
});
it("waits for the commit signal when a same-path search and hash navigation changes the tree", async () => {
const initialElements = createResolvedElements("route:/hash", "/", null, {
"page:/hash": React.createElement("main", null, "old hash page"),
});
const nextElements = createResolvedElements("route:/hash", "/", null, {
"page:/hash": React.createElement("main", null, "new hash page"),
});
const initialState = createState({
elements: initialElements,
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/hash?old=1#old-content",
{},
),
routeId: "route:/hash",
});
const { controller, detach } = createControllerHarness(initialState);
const commitEffect = vi.fn();
const scrollIntent = beginAppRouterScrollIntent("#new-content");
stubWindow("https://example.com/hash?old=1#old-content");
try {
const navId = controller.beginNavigation();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => commitEffect,
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/hash?new=1#new-content",
{},
),
nextElements: Promise.resolve(nextElements),
operationLane: "navigation",
params: {},
pendingRouterState: controller.beginPendingBrowserRouterState(),
previousNextUrl: null,
scrollIntent,
targetHref: "https://example.com/hash?new=1#new-content",
navId,
});
await expect(
Promise.race([
renderPromise,
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 25)),
]),
).resolves.toBe("timeout");
expect(commitEffect).not.toHaveBeenCalled();
} finally {
clearAppRouterScrollIntent();
detach();
}
});
it("does not settle an unchanged search and hash render after a newer navigation starts", async () => {
const initialElements = createResolvedElements("route:/hash", "/", null, {
"page:/hash": React.createElement("main", null, "hash page"),
});
const initialState = createState({
elements: initialElements,
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/hash", {}),
routeId: "route:/hash",
});
const { controller, detach } = createControllerHarness(initialState);
const commitEffect = vi.fn();
const scrollIntent = beginAppRouterScrollIntent("#hash-160");
stubWindow("https://example.com/hash");
try {
const navId = controller.beginNavigation();
const pendingRouterState = controller.beginPendingBrowserRouterState();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => commitEffect,
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/hash?with-query-param#hash-160",
{},
),
nextElements: Promise.resolve(initialElements),
operationLane: "navigation",
params: {},
pendingRouterState,
previousNextUrl: null,
scrollIntent,
targetHref: "https://example.com/hash?with-query-param#hash-160",
navId,
});
for (let attempts = 0; attempts < 5 && !pendingRouterState.settled; attempts++) {
await Promise.resolve();
}
expect(pendingRouterState.settled).toBe(true);
controller.beginNavigation();
await expect(
Promise.race([
renderPromise,
new Promise<"timeout">((resolve) => setTimeout(() => resolve("timeout"), 25)),
]),
).resolves.toBe("timeout");
expect(commitEffect).not.toHaveBeenCalled();
} finally {
clearAppRouterScrollIntent();
detach();
}
});
it("preserves a newer same-URL failure target when an older navigation is discarded", async () => {
const { controller, detach } = createControllerHarness();
stubWindow("https://example.com/initial");
vi.stubEnv("__NEXT_APP_NAV_FAIL_HANDLING", "true");
let resolveNextElements!: (value: AppElements) => void;
try {
stageAppNavigationFailureTarget("/dashboard");
const olderTarget = window.next?.__pendingUrl;
const olderNavId = controller.beginNavigation();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/initial", {}),
nextElements: new Promise<AppElements>((resolve) => {
resolveNextElements = resolve;
}),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: olderNavId,
});
controller.beginNavigation();
stageAppNavigationFailureTarget("/dashboard");
const newerTarget = window.next?.__pendingUrl;
expect(newerTarget).not.toBe(olderTarget);
resolveNextElements(
createResolvedElements("route:/dashboard", "/", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
await expect(renderPromise).resolves.toBe("no-commit");
expect(window.next?.__pendingUrl).toBe(newerTarget);
} finally {
detach();
vi.unstubAllEnvs();
}
});
it("renderNavigationPayload stays pending until NavigationCommitSignal settles the commit", async () => {
const { controller, detach, stateRef } = createControllerHarness();
const commitEffect = vi.fn();
const nextElements = Promise.resolve(
createResolvedElements("route:/dashboard", "/", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
try {
const navId = controller.beginNavigation();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => commitEffect,
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId,
});
// Yield enough microticks for the async function to reach dispatch
// and return the committed promise.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
// Pre-paint effect is queued but not yet run (drainPrePaintEffects
// only fires inside NavigationCommitSignal's useLayoutEffect).
expect(commitEffect).not.toHaveBeenCalled();
// The promise must not resolve — NavigationCommitSignal has not
// mounted, so resolveCommittedNavigations has no way to fire.
const settled = await Promise.race([
renderPromise.then(() => true),
Promise.resolve().then(() => false),
]);
expect(settled).toBe(false);
} finally {
detach();
}
});
it("does not clear a newer navigation failure target when an older render commits", async () => {
const { controller, detach, stateRef } = createControllerHarness();
vi.stubEnv("__NEXT_APP_NAV_FAIL_HANDLING", "true");
vi.stubGlobal("window", {
location: {
href: "https://example.com/initial",
origin: "https://example.com",
},
next: {},
});
try {
stageAppNavigationFailureTarget("/older");
const olderNavId = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: Promise.resolve(
createResolvedElements("route:/older", "/", null, {
"page:/older": React.createElement("main", null, "older"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/older",
navId: olderNavId,
});
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
let resolveNewer!: (elements: AppElements) => void;
stageAppNavigationFailureTarget("/newer");
const newerNavId = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: new Promise<AppElements>((resolve) => {
resolveNewer = resolve;
}),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/newer",
navId: newerNavId,
});
controller.clearCommittedNavigationFailureTargets(1);
expect(window.next?.__pendingUrl?.pathname).toBe("/newer");
resolveNewer(
createResolvedElements("route:/newer", "/", null, {
"page:/newer": React.createElement("main", null, "newer"),
}),
);
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
controller.clearCommittedNavigationFailureTargets(2);
expect(window.next?.__pendingUrl).toBeUndefined();
} finally {
detach();
vi.unstubAllEnvs();
vi.unstubAllGlobals();
}
});
it("dispatches same-URL server action payloads into the browser router state", async () => {
const initialState = createState({
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {
tab: "profile",
}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
const { assign } = stubWindow("https://example.com/settings");
const nextElements = Promise.resolve(
createResolvedElements("route:/settings/account", "/", null, {
"page:/settings/account": React.createElement("main", null, "account"),
}),
);
try {
const result = await controller.commitSameUrlNavigatePayload(
nextElements,
stateRef.current.navigationSnapshot,
{
data: "server-action-result",
ok: true,
},
);
expect(result).toBe("server-action-result");
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/settings/account");
expect(stateRef.current.previousNextUrl).toBeNull();
expect(stateRef.current.visibleCommitVersion).toBe(1);
expect(stateRef.current.activeOperation).toMatchObject({
lane: "server-action",
startedVisibleCommitVersion: 0,
state: "committed",
visibleCommitVersion: 1,
});
} finally {
detach();
}
});
it("syncs cleared previousNextUrl after same-URL server action commits", async () => {
const interception = createInterceptionProof("/feed", "/photos/42");
const initialState = createState({
interception,
interceptionContext: "/feed",
previousNextUrl: "/feed",
rootLayoutTreePath: "/",
routeId: "route:/feed",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/photos/42", {}),
});
const syncHistoryStatePreviousNextUrl = vi.fn();
const { controller, detach, stateRef } = createControllerHarness(initialState, {
syncHistoryStatePreviousNextUrl,
});
const { assign } = stubWindow("https://example.com/photos/42");
const nextElements = Promise.resolve(
createResolvedElements("route:/photos/42", "/", null, {
"page:/photos/42": React.createElement("main", null, "photo page"),
}),
);
try {
await controller.commitSameUrlNavigatePayload(
nextElements,
stateRef.current.navigationSnapshot,
undefined,
stateRef.current,
{ targetHref: "https://example.com/photos/42" },
);
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/photos/42");
expect(stateRef.current.previousNextUrl).toBeNull();
expect(syncHistoryStatePreviousNextUrl).toHaveBeenCalledTimes(1);
expect(syncHistoryStatePreviousNextUrl).toHaveBeenCalledWith(
null,
stateRef.current.bfcacheIds,
);
} finally {
detach();
}
});
it("does not let older same-URL server action payloads overwrite newer visible commits", async () => {
const initialState = createState({
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {
tab: "profile",
}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
const { assign } = stubWindow("https://example.com/settings");
let resolveOlderPayload!: (elements: AppElements) => void;
const olderPayload = new Promise<AppElements>((resolve) => {
resolveOlderPayload = resolve;
});
try {
const olderResult = controller.commitSameUrlNavigatePayload(
olderPayload,
stateRef.current.navigationSnapshot,
{
data: "older-action-result",
ok: true,
},
);
const newerResult = await controller.commitSameUrlNavigatePayload(
Promise.resolve(
createResolvedElements("route:/settings/newer", "/", null, {
"page:/settings/newer": React.createElement("main", null, "newer"),
}),
),
stateRef.current.navigationSnapshot,
{
data: "newer-action-result",
ok: true,
},
);
expect(newerResult).toBe("newer-action-result");
expect(stateRef.current.routeId).toBe("route:/settings/newer");
expect(stateRef.current.visibleCommitVersion).toBe(1);
resolveOlderPayload(
createResolvedElements("route:/settings/older", "/", null, {
"page:/settings/older": React.createElement("main", null, "older"),
}),
);
await expect(olderResult).resolves.toBe("older-action-result");
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/settings/newer");
expect(stateRef.current.visibleCommitVersion).toBe(1);
expect(stateRef.current.activeOperation).toMatchObject({
lane: "server-action",
startedVisibleCommitVersion: 0,
state: "committed",
visibleCommitVersion: 1,
});
} finally {
detach();
}
});
it("reports discarded revalidating server action payloads for current-state refresh", async () => {
const initialState = createState({
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {
tab: "profile",
}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
const { assign } = stubWindow("https://example.com/settings");
const onDiscardedRevalidation = vi.fn();
let resolveOlderPayload!: (elements: AppElements) => void;
const olderPayload = new Promise<AppElements>((resolve) => {
resolveOlderPayload = resolve;
});
try {
const olderResult = controller.commitSameUrlNavigatePayload(
olderPayload,
stateRef.current.navigationSnapshot,
{
data: "older-action-result",
ok: true,
},
undefined,
{
onDiscardedRevalidation,
revalidation: "staticAndDynamic",
},
);
await controller.commitSameUrlNavigatePayload(
Promise.resolve(
createResolvedElements("route:/settings/newer", "/", null, {
"page:/settings/newer": React.createElement("main", null, "newer"),
}),
),
stateRef.current.navigationSnapshot,
{
data: "newer-action-result",
ok: true,
},
);
resolveOlderPayload(
createResolvedElements("route:/settings/older", "/", null, {
"page:/settings/older": React.createElement("main", null, "older"),
}),
);
await expect(olderResult).resolves.toBe("older-action-result");
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/settings/newer");
expect(stateRef.current.visibleCommitVersion).toBe(1);
expect(onDiscardedRevalidation).toHaveBeenCalledTimes(1);
} finally {
detach();
}
});
it("discards revalidating server actions that started before an in-flight navigation", async () => {
const initialState = createState({
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {
tab: "profile",
}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
const { assign } = stubWindow("https://example.com/settings");
const onDiscardedRevalidation = vi.fn();
const actionInitiationNavigationId = controller.getActiveNavigationId();
const actionInitiationState = stateRef.current;
let resolvePayload!: (elements: AppElements) => void;
const payload = new Promise<AppElements>((resolve) => {
resolvePayload = resolve;
});
try {
const result = controller.commitSameUrlNavigatePayload(
payload,
actionInitiationState.navigationSnapshot,
{
data: "action-result",
ok: true,
},
actionInitiationState,
{
onDiscardedRevalidation,
revalidation: "dynamicOnly",
startedNavigationId: actionInitiationNavigationId,
},
);
controller.beginNavigation();
resolvePayload(
createResolvedElements("route:/settings/action", "/", null, {
"page:/settings/action": React.createElement("main", null, "action"),
}),
);
await expect(result).resolves.toBe("action-result");
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/settings");
expect(stateRef.current.visibleCommitVersion).toBe(0);
expect(onDiscardedRevalidation).toHaveBeenCalledTimes(1);
} finally {
detach();
}
});
it("revalidates same-URL server action commits immediately before dispatch", async () => {
const controller = createAppBrowserNavigationController();
const initialState = createState({
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {
tab: "profile",
}),
});
let currentState = initialState;
let staleBeforeDispatch = false;
const stateRef = {
get current(): AppRouterState {
if (staleBeforeDispatch) {
staleBeforeDispatch = false;
queueMicrotask(() => {
currentState = createState({
routeId: "route:/settings/newer",
visibleCommitVersion: 1,
});
});
}
return currentState;
},
set current(value: AppRouterState) {
currentState = value;
},
};
const setBrowserRouterState = vi.fn((value: AppRouterState | Promise<AppRouterState>) => {
if (!(value instanceof Promise)) {
stateRef.current = value;
}
});
const detach = controller.attachBrowserRouterState(setBrowserRouterState, stateRef);
const { assign } = stubWindow("https://example.com/settings");
let resolvePayload!: (elements: AppElements) => void;
const payload = new Promise<AppElements>((resolve) => {
resolvePayload = resolve;
});
try {
const result = controller.commitSameUrlNavigatePayload(
payload,
stateRef.current.navigationSnapshot,
{
data: "older-action-result",
ok: true,
},
);
staleBeforeDispatch = true;
resolvePayload(
createResolvedElements("route:/settings/older", "/", null, {
"page:/settings/older": React.createElement("main", null, "older"),
}),
);
await expect(result).resolves.toBe("older-action-result");
expect(assign).not.toHaveBeenCalled();
expect(currentState.routeId).toBe("route:/settings/newer");
expect(currentState.visibleCommitVersion).toBe(1);
} finally {
detach();
}
});
it("uses the server-action initiation state when the response is processed after a newer commit", async () => {
const initialState = createState({
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {
tab: "profile",
}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
const { assign } = stubWindow("https://example.com/settings");
const actionInitiationState = stateRef.current;
try {
const newerResult = await controller.commitSameUrlNavigatePayload(
Promise.resolve(
createResolvedElements("route:/settings/newer", "/", null, {
"page:/settings/newer": React.createElement("main", null, "newer"),
}),
),
stateRef.current.navigationSnapshot,
{
data: "newer-action-result",
ok: true,
},
);
expect(newerResult).toBe("newer-action-result");
expect(stateRef.current.routeId).toBe("route:/settings/newer");
expect(stateRef.current.visibleCommitVersion).toBe(1);
const olderResult = await controller.commitSameUrlNavigatePayload(
Promise.resolve(
createResolvedElements("route:/settings/older", "/", null, {
"page:/settings/older": React.createElement("main", null, "older"),
}),
),
actionInitiationState.navigationSnapshot,
{
data: "older-action-result",
ok: true,
},
actionInitiationState,
);
expect(olderResult).toBe("older-action-result");
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/settings/newer");
expect(stateRef.current.visibleCommitVersion).toBe(1);
} finally {
detach();
}
});
it("does not hard-navigate same-URL server action payloads from snapshot root topology alone", async () => {
const actionTargetHref = "https://example.com/marketing?from=action";
const initialState = createState({
rootLayoutTreePath: "/(marketing)",
routeId: "route:/marketing",
navigationSnapshot: createClientNavigationRenderSnapshot(actionTargetHref, {}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
const { assign } = stubWindow("https://example.com/current");
const nextElements = Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
try {
const result = await controller.commitSameUrlNavigatePayload(
nextElements,
stateRef.current.navigationSnapshot,
undefined,
undefined,
{
targetHref: actionTargetHref,
},
);
expect(result).toBeUndefined();
expect(assign).not.toHaveBeenCalled();
expect(stateRef.current.routeId).toBe("route:/dashboard");
} finally {
detach();
}
});
});
describe("app browser navigation lifecycle settlement", () => {
it("prepares authoritative layout reuse from the state before a detached commit", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const projectLayoutId = AppElementsWire.encodeLayoutId("/projects/[projectId]");
const navigationInitiationState = createState({
bfcacheIds: { [rootLayoutId]: "0" },
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
[rootLayoutId]: rootLayout,
"page:/dashboard": React.createElement("main", null, "dashboard"),
},
[rootLayoutId],
),
layoutIds: [rootLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/dashboard", {}),
routeId: "route:/dashboard",
});
const routeManifest = createTestRouteManifest([
{
id: "route:/dashboard",
layoutIds: [rootLayoutId],
pattern: "/dashboard",
rootBoundaryId: "root-boundary:/",
},
{
id: "route:/projects/[projectId]",
layoutIds: [rootLayoutId, projectLayoutId],
pattern: "/projects/[projectId]",
rootBoundaryId: "root-boundary:/",
},
]);
const { controller, detach, stateRef, waitForNextVisibleCommitDispatch } =
createControllerHarness(navigationInitiationState, {
getRouteManifest: () => routeManifest,
});
const targetHref = "https://example.com/projects/b";
const navigationSnapshot = createClientNavigationRenderSnapshot(targetHref, {
projectId: "b",
});
try {
const navId = controller.beginNavigation();
await dispatchTestNavigationPayload({
controller,
navigationCommitKind: "detached",
navigationInitiationState,
navigationSnapshot,
navId,
nextElements: Promise.resolve(
createResolvedElements(
"route:/projects/[projectId]",
"/",
null,
{
[APP_SKIPPED_LAYOUT_IDS_KEY]: [rootLayoutId],
[projectLayoutId]: React.createElement("section", null, "stale project a"),
"page:/projects/[projectId]": React.createElement("main", null, "project b"),
},
[rootLayoutId, projectLayoutId],
),
),
params: { projectId: "b" },
targetHref,
waitForNextVisibleCommitDispatch,
});
const detachedProjectLayoutId = stateRef.current.bfcacheIds[projectLayoutId];
expect(stateRef.current.elements[projectLayoutId]).toMatchObject({
props: { children: "stale project a" },
});
await dispatchTestNavigationPayload({
controller,
navigationCommitKind: "authoritative",
navigationInitiationState,
navigationSnapshot,
navId,
nextElements: Promise.resolve(
createResolvedElements(
"route:/projects/[projectId]",
"/",
null,
{
[APP_SKIPPED_LAYOUT_IDS_KEY]: [rootLayoutId],
[projectLayoutId]: React.createElement("section", null, "project b layout"),
"page:/projects/[projectId]": React.createElement("main", null, "project b"),
},
[rootLayoutId, projectLayoutId],
),
),
params: { projectId: "b" },
targetHref,
waitForNextVisibleCommitDispatch,
});
expect(stateRef.current.elements[projectLayoutId]).toMatchObject({
props: { children: "project b layout" },
});
expect(stateRef.current.bfcacheIds[projectLayoutId]).not.toBe(detachedProjectLayoutId);
expect(stateRef.current.elements[rootLayoutId]).toBe(rootLayout);
expect(stateRef.current.bfcacheIds[rootLayoutId]).toBe("0");
} finally {
detach();
}
});
it("preserves a dynamic layout identity across detached same-param sibling payloads", async () => {
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const projectLayoutId = AppElementsWire.encodeLayoutId("/projects/[projectId]");
const projectLayout = React.createElement("section", null, "project a client state");
const navigationInitiationState = createState({
bfcacheIds: {
[rootLayoutId]: "0",
[projectLayoutId]: "_b_2_",
},
elements: createResolvedElements(
"route:/projects/[projectId]/overview",
"/",
null,
{
[rootLayoutId]: React.createElement("div", null, "root layout"),
[projectLayoutId]: projectLayout,
"page:/projects/[projectId]/overview": React.createElement("main", null, "overview"),
},
[rootLayoutId, projectLayoutId],
),
layoutIds: [rootLayoutId, projectLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/projects/a/overview",
{ projectId: "a" },
),
routeId: "route:/projects/[projectId]/overview",
});
const routeManifest = createTestRouteManifest([
{
id: "route:/projects/[projectId]/overview",
layoutIds: [rootLayoutId, projectLayoutId],
pattern: "/projects/[projectId]/overview",
rootBoundaryId: "root-boundary:/",
},
{
id: "route:/projects/[projectId]/activity",
layoutIds: [rootLayoutId, projectLayoutId],
pattern: "/projects/[projectId]/activity",
rootBoundaryId: "root-boundary:/",
},
]);
const { controller, detach, stateRef, waitForNextVisibleCommitDispatch } =
createControllerHarness(navigationInitiationState, {
getRouteManifest: () => routeManifest,
});
const targetHref = "https://example.com/projects/a/activity";
const navigationSnapshot = createClientNavigationRenderSnapshot(targetHref, {
projectId: "a",
});
const createPayload = (content: string) =>
Promise.resolve(
createResolvedElements(
"route:/projects/[projectId]/activity",
"/",
null,
{
[APP_SKIPPED_LAYOUT_IDS_KEY]: [rootLayoutId, projectLayoutId],
"page:/projects/[projectId]/activity": React.createElement("main", null, content),
},
[rootLayoutId, projectLayoutId],
),
);
try {
const navId = controller.beginNavigation();
await dispatchTestNavigationPayload({
controller,
navigationCommitKind: "detached",
navigationInitiationState,
navigationSnapshot,
navId,
nextElements: createPayload("optimistic activity"),
params: { projectId: "a" },
targetHref,
waitForNextVisibleCommitDispatch,
});
await dispatchTestNavigationPayload({
controller,
navigationCommitKind: "authoritative",
navigationInitiationState,
navigationSnapshot,
navId,
nextElements: createPayload("authoritative activity"),
params: { projectId: "a" },
targetHref,
waitForNextVisibleCommitDispatch,
});
expect(stateRef.current.elements[projectLayoutId]).toBe(projectLayout);
expect(stateRef.current.bfcacheIds[projectLayoutId]).toBe("_b_2_");
expect(stateRef.current.elements["page:/projects/[projectId]/activity"]).toMatchObject({
props: { children: "authoritative activity" },
});
} finally {
detach();
}
});
it("replaces a detached slot when its dynamic layout owner was introduced optimistically", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const projectLayoutId = AppElementsWire.encodeLayoutId("/projects/[projectId]");
const sidebarSlotId = AppElementsWire.encodeSlotId("sidebar", "/projects/[projectId]");
const slotBindings: readonly AppElementsSlotBinding[] = [
{ ownerLayoutId: projectLayoutId, slotId: sidebarSlotId, state: "default" },
];
const navigationInitiationState = createState({
bfcacheIds: { [rootLayoutId]: "0" },
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
[rootLayoutId]: rootLayout,
"page:/dashboard": React.createElement("main", null, "dashboard"),
},
[rootLayoutId],
),
layoutIds: [rootLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/dashboard", {}),
routeId: "route:/dashboard",
});
const routeManifest = createTestRouteManifest([
{
id: "route:/dashboard",
layoutIds: [rootLayoutId],
pattern: "/dashboard",
rootBoundaryId: "root-boundary:/",
},
{
id: "route:/projects/[projectId]",
layoutIds: [rootLayoutId, projectLayoutId],
pattern: "/projects/[projectId]",
rootBoundaryId: "root-boundary:/",
slotBindings,
},
]);
const { controller, detach, stateRef, waitForNextVisibleCommitDispatch } =
createControllerHarness(navigationInitiationState, {
getRouteManifest: () => routeManifest,
});
const targetHref = "https://example.com/projects/b";
const navigationSnapshot = createClientNavigationRenderSnapshot(targetHref, {
projectId: "b",
});
const createPayload = (project: string) =>
Promise.resolve(
createResolvedElements(
"route:/projects/[projectId]",
"/",
null,
{
[APP_SKIPPED_LAYOUT_IDS_KEY]: [rootLayoutId],
[projectLayoutId]: React.createElement("section", null, `${project} layout`),
[sidebarSlotId]: React.createElement("aside", null, `${project} sidebar`),
"page:/projects/[projectId]": React.createElement("main", null, "project b"),
},
[rootLayoutId, projectLayoutId],
slotBindings,
),
);
try {
const navId = controller.beginNavigation();
await dispatchTestNavigationPayload({
controller,
navigationCommitKind: "detached",
navigationInitiationState,
navigationSnapshot,
navId,
nextElements: createPayload("stale project a"),
params: { projectId: "b" },
targetHref,
waitForNextVisibleCommitDispatch,
});
const detachedOwnerId = stateRef.current.bfcacheIds[projectLayoutId];
const detachedSlotId = stateRef.current.bfcacheIds[sidebarSlotId];
expect(stateRef.current.elements[sidebarSlotId]).toMatchObject({
props: { children: "stale project a sidebar" },
});
await dispatchTestNavigationPayload({
controller,
navigationCommitKind: "authoritative",
navigationInitiationState,
navigationSnapshot,
navId,
nextElements: createPayload("project b"),
params: { projectId: "b" },
targetHref,
waitForNextVisibleCommitDispatch,
});
expect(stateRef.current.elements[sidebarSlotId]).toMatchObject({
props: { children: "project b sidebar" },
});
expect(stateRef.current.bfcacheIds[projectLayoutId]).not.toBe(detachedOwnerId);
expect(stateRef.current.bfcacheIds[sidebarSlotId]).not.toBe(detachedSlotId);
expect(stateRef.current.elements[rootLayoutId]).toBe(rootLayout);
expect(stateRef.current.bfcacheIds[rootLayoutId]).toBe("0");
} finally {
detach();
}
});
it("lets an authoritative payload replace a detached commit from the same navigation", async () => {
const { controller, detach, stateRef } = createControllerHarness();
try {
const navId = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/items?filter=active",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/items", "/", null, {
"page:/items": React.createElement("main", null, "optimistic"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/items?filter=active",
navId,
navigationCommitKind: "detached",
});
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.elements["page:/items"]).toMatchObject({
props: { children: "optimistic" },
});
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/items?filter=active",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/items", "/", null, {
"page:/items": React.createElement("main", null, "authoritative"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/items?filter=active",
navId,
navigationCommitKind: "authoritative",
});
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.elements["page:/items"]).toMatchObject({
props: { children: "authoritative" },
});
expect(stateRef.current.activeOperation).toMatchObject({
navigationId: navId,
state: "committed",
});
const lateDetachedOutcome = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/items?filter=active",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/items", "/", null, {
"page:/items": React.createElement("main", null, "late detached"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/items?filter=active",
navId,
navigationCommitKind: "detached",
});
await expect(lateDetachedOutcome).resolves.toBe("no-commit");
expect(stateRef.current.elements["page:/items"]).toMatchObject({
props: { children: "authoritative" },
});
} finally {
detach();
}
});
it("most recent navigation commits when three are started and payloads resolve in reverse order", async () => {
const { controller, detach, stateRef } = createControllerHarness();
let resolveA!: (elements: AppElements) => void;
let resolveB!: (elements: AppElements) => void;
const payloadA = new Promise<AppElements>((r) => {
resolveA = r;
});
const payloadB = new Promise<AppElements>((r) => {
resolveB = r;
});
const payloadC = Promise.resolve(
createResolvedElements("route:/c", "/", null, {
"page:/c": React.createElement("main", null, "C"),
}),
);
const effectsRun: string[] = [];
try {
// Start three navigations. Only C is the current (winning) one.
const navA = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => {
effectsRun.push("A");
return () => {};
},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: payloadA,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/a",
navId: navA,
});
const navB = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => {
effectsRun.push("B");
return () => {};
},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: payloadB,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/b",
navId: navB,
});
const navC = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => {
effectsRun.push("C");
return () => {};
},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: payloadC,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/c",
navId: navC,
});
// Yield so C's async payload resolves and state is committed.
// renderNavigationPayload returns a promise that settles only when
// NavigationCommitSignal fires (a React component not mounted in
// unit tests). The state mutation through dispatchApprovedVisibleCommit is
// applied during React.startTransition's action, so we verify via stateRef.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/c");
// B resolves after C was committed — stale, must be skipped.
resolveB(
createResolvedElements("route:/b", "/", null, {
"page:/b": React.createElement("main", null, "B"),
}),
);
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/c");
// A resolves last — most stale, must be skipped.
resolveA(
createResolvedElements("route:/a", "/", null, {
"page:/a": React.createElement("main", null, "A"),
}),
);
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/c");
// Only C's commit effect was queued. A and B were classified as
// "skip" before createNavigationCommitEffect ever ran.
expect(effectsRun).toEqual(["C"]);
} finally {
detach();
}
});
it("stale cross-root navigation is skipped instead of hard-navigating", async () => {
// A navigation that crosses a root-layout boundary requires a hard
// navigation. But a stale navigation (superseded by a newer one) must NOT
// hard-navigate, even if the payload says the roots differ. A "skip" for
// the stale operation must take priority over a "hard-navigate" for a
// navigation that is no longer current.
const { controller, detach, stateRef } = createControllerHarness(
createState({ rootLayoutTreePath: "/(marketing)" }),
);
const { assign } = stubWindow("https://example.com/marketing");
let resolveCrossRoot!: (elements: AppElements) => void;
const crossRootPayload = new Promise<AppElements>((r) => {
resolveCrossRoot = r;
});
try {
// Start cross-root navigation A (deferred, /(marketing) → /(dashboard)).
const navA = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: crossRootPayload,
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: navA,
});
// Start new navigation B (same root). B advances activeNavigationId past A.
const navB = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: Promise.resolve(
createResolvedElements("route:/marketing/settings", "/(marketing)", null, {
"page:/marketing/settings": React.createElement("main", null, "settings"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/marketing/settings",
navId: navB,
});
// Yield so B's async payload resolves and state commits.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/marketing/settings");
// Now resolve the stale cross-root payload. It has a different root
// layout, but the navigation it belongs to is no longer current.
resolveCrossRoot(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
);
await Promise.resolve();
await Promise.resolve();
// Must NOT have hard-navigated. The stale operation is simply skipped.
expect(assign).not.toHaveBeenCalled();
// The visible route must be B's, not A's stale payload.
expect(stateRef.current.routeId).toBe("route:/marketing/settings");
} finally {
detach();
}
});
it("keeps a newer visible commit when an older refresh resolves late", async () => {
const { controller, detach, stateRef } = createControllerHarness();
let resolveRefresh!: (elements: AppElements) => void;
const refreshPayload = new Promise<AppElements>((resolve) => {
resolveRefresh = resolve;
});
try {
const refreshNav = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: undefined,
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: refreshPayload,
operationLane: "refresh",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/initial",
navId: refreshNav,
});
await controller.hmrReplaceTree(
Promise.resolve(
createResolvedElements("route:/hmr", "/", null, {
"page:/hmr": React.createElement("main", null, "hmr"),
}),
),
stateRef.current.navigationSnapshot,
);
expect(stateRef.current.routeId).toBe("route:/hmr");
resolveRefresh(
createResolvedElements("route:/initial-refreshed", "/", null, {
"page:/initial": React.createElement("main", null, "refreshed"),
}),
);
await Promise.resolve();
await Promise.resolve();
expect(stateRef.current.routeId).toBe("route:/hmr");
expect(stateRef.current.activeOperation).toMatchObject({
lane: "hmr",
state: "committed",
visibleCommitVersion: 1,
});
} finally {
detach();
}
});
it("settles pending state without patching visible UI when an older traverse resolves late", async () => {
const { controller, detach, stateRef } = createControllerHarness();
let resolveTraverse!: (elements: AppElements) => void;
const traversePayload = new Promise<AppElements>((resolve) => {
resolveTraverse = resolve;
});
try {
const traversePendingState = controller.beginPendingBrowserRouterState();
const traverseNav = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "traverse",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: undefined,
navigationSnapshot: stateRef.current.navigationSnapshot,
nextElements: traversePayload,
operationLane: "traverse",
params: {},
pendingRouterState: traversePendingState,
previousNextUrl: null,
targetHref: "https://example.com/previous",
navId: traverseNav,
});
await controller.hmrReplaceTree(
Promise.resolve(
createResolvedElements("route:/hmr", "/", null, {
"page:/hmr": React.createElement("main", null, "hmr"),
}),
),
stateRef.current.navigationSnapshot,
);
resolveTraverse(
createResolvedElements("route:/previous", "/", null, {
"page:/previous": React.createElement("main", null, "previous"),
}),
);
await Promise.resolve();
await Promise.resolve();
await expect(traversePendingState.promise).resolves.toBe(stateRef.current);
expect(traversePendingState.settled).toBe(true);
expect(stateRef.current.routeId).toBe("route:/hmr");
expect(stateRef.current.activeOperation).toMatchObject({
lane: "hmr",
state: "committed",
visibleCommitVersion: 1,
});
} finally {
detach();
}
});
it("resolveAndClassifyNavigationCommit classifies skip when IDs have diverged", async () => {
const result = await resolveAndClassifyNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
activeNavigationId: 9,
currentState: createState(),
navigationSnapshot: createState().navigationSnapshot,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/")),
operationLane: "navigation",
renderId: 3,
startedNavigationId: 5,
targetHref: "https://example.com/dashboard",
type: "navigate",
});
expect(result.decision.disposition).toBe("no-commit");
expect(result.pending.routeId).toBe("route:/dashboard");
});
it("uses the active navigation getter after the payload resolves", async () => {
const currentState = createState();
let activeNavigationId = 5;
let resolvePayload!: (elements: AppElements) => void;
const payload = new Promise<AppElements>((resolve) => {
resolvePayload = resolve;
});
const resultPromise = resolveAndClassifyNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
activeNavigationId,
currentState,
getActiveNavigationId: () => activeNavigationId,
navigationSnapshot: currentState.navigationSnapshot,
nextElements: payload,
operationLane: "server-action",
renderId: 24,
startedNavigationId: 5,
targetHref: "https://example.com/dashboard",
type: "navigate",
});
activeNavigationId = 9;
resolvePayload(createResolvedElements("route:/dashboard", "/"));
const result = await resultPromise;
expect(result.decision.disposition).toBe("no-commit");
expect(result.approvedCommit).toBeNull();
expect(result.trace.entries).toEqual([
{
code: NavigationTraceTransactionCodes.noCommit,
fields: {
operationLane: "server-action",
pendingOperationId: 24,
startedVisibleCommitVersion: 0,
},
},
{
code: NavigationTraceReasonCodes.staleOperation,
fields: {
activeNavigationId: 9,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 0,
nextRootLayoutTreePath: "/",
startedNavigationId: 5,
startedVisibleCommitVersion: 0,
targetHref: "https://example.com/dashboard",
},
},
]);
});
it("uses the approval state getter after the payload resolves", async () => {
const startedState = createState({ visibleCommitVersion: 0 });
let approvalState = startedState;
let resolvePayload!: (elements: AppElements) => void;
const payload = new Promise<AppElements>((resolve) => {
resolvePayload = resolve;
});
const resultPromise = resolveAndClassifyNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
activeNavigationId: 8,
currentState: startedState,
getCurrentStateForApproval: () => approvalState,
navigationSnapshot: startedState.navigationSnapshot,
nextElements: payload,
operationLane: "server-action",
renderId: 25,
startedNavigationId: 8,
targetHref: "https://example.com/dashboard",
type: "navigate",
});
approvalState = createState({
routeId: "route:/newer",
visibleCommitVersion: 1,
});
resolvePayload(createResolvedElements("route:/dashboard", "/"));
const result = await resultPromise;
expect(result.decision.disposition).toBe("no-commit");
expect(result.approvedCommit).toBeNull();
expect(result.trace.entries).toEqual([
{
code: NavigationTraceTransactionCodes.noCommit,
fields: {
operationLane: "server-action",
pendingOperationId: 25,
startedVisibleCommitVersion: 0,
},
},
{
code: NavigationTraceReasonCodes.staleOperation,
fields: {
activeNavigationId: 8,
currentRootLayoutTreePath: "/",
currentVisibleCommitVersion: 1,
nextRootLayoutTreePath: "/",
startedNavigationId: 8,
startedVisibleCommitVersion: 0,
targetHref: "https://example.com/dashboard",
},
},
]);
});
it("failed payload cleanly settles the pending router state without leaving it hanging", async () => {
const { controller, detach } = createControllerHarness();
const clearSpy = vi.spyOn(navigationShim, "clearPendingPathname").mockImplementation(() => {});
const pendingRouterState = controller.beginPendingBrowserRouterState();
try {
const navId = controller.beginNavigation();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/initial", {}),
nextElements: Promise.reject(new Error("RSC fetch failed")),
operationLane: "navigation",
params: {},
pendingRouterState,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId,
});
await expect(renderPromise).rejects.toThrow("RSC fetch failed");
// The pending router promise must be settled so callers don't hang.
await expect(pendingRouterState.promise).resolves.toBeDefined();
expect(pendingRouterState.settled).toBe(true);
} finally {
clearSpy.mockRestore();
detach();
}
});
});
describe("app browser root-layout hard navigation", () => {
it("renderNavigationPayload calls window.location.assign when root layout changes", async () => {
const { controller, detach } = createControllerHarness(
createState({ rootLayoutTreePath: "/(marketing)" }),
{ getRouteManifest: () => createRootChangeRouteManifest() },
);
const { assign } = stubWindow("https://example.com/marketing");
const createNavigationCommitEffect = vi.fn();
try {
const navId = controller.beginNavigation();
const renderPromise = renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect,
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId,
});
await expect(renderPromise).resolves.toBe("hard-navigate");
expect(assign).toHaveBeenCalledTimes(1);
expect(assign).toHaveBeenCalledWith("https://example.com/dashboard");
expect(createNavigationCommitEffect).not.toHaveBeenCalled();
} finally {
detach();
}
});
it("hard-navigate settles the pending router state before navigating away", async () => {
const { controller, detach } = createControllerHarness(
createState({ rootLayoutTreePath: "/(marketing)" }),
{ getRouteManifest: () => createRootChangeRouteManifest() },
);
const { assign } = stubWindow("https://example.com/marketing");
const pendingRouterState = controller.beginPendingBrowserRouterState();
assign.mockImplementation(() => {
expect(pendingRouterState.settled).toBe(true);
});
try {
const navId = controller.beginNavigation();
void renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId,
});
// Yield so the async function runs the settle+hard-navigate path.
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(assign).toHaveBeenCalledTimes(1);
await expect(pendingRouterState.promise).resolves.toBeDefined();
} finally {
detach();
}
});
it("blocks a repeated same-target root-boundary hard navigation to prevent reload loops", async () => {
const { controller, detach } = createControllerHarness(
createState({ rootLayoutTreePath: "/(marketing)" }),
{ getRouteManifest: () => createRootChangeRouteManifest() },
);
const { assign, storage } = stubWindow("https://example.com/marketing");
try {
const firstNavId = controller.beginNavigation();
await expect(
renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: firstNavId,
}),
).resolves.toBe("hard-navigate");
expect(assign).toHaveBeenCalledTimes(1);
expect(storage.size).toBe(1);
assign.mockClear();
window.location.href = "https://example.com/dashboard";
const secondNavId = controller.beginNavigation();
await expect(
renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: secondNavId,
}),
).resolves.toBe("no-commit");
expect(assign).not.toHaveBeenCalled();
expect(storage.size).toBe(0);
} finally {
detach();
}
});
it("blocks a repeated same-target hard-navigation fallback outside visible commits", () => {
const controller = createAppBrowserNavigationController();
const { assign } = stubWindow("https://example.com/dashboard");
expect(controller.performHardNavigation("https://example.com/dashboard")).toBe(true);
expect(assign).toHaveBeenCalledTimes(1);
assign.mockClear();
expect(controller.performHardNavigation("https://example.com/dashboard")).toBe(false);
expect(assign).not.toHaveBeenCalled();
});
it("allows cross-page hard navigation when the stored guard target is not the current URL", async () => {
const { controller, detach } = createControllerHarness(
createState({ rootLayoutTreePath: "/(marketing)" }),
{ getRouteManifest: () => createRootChangeRouteManifest() },
);
const { assign } = stubWindow("https://example.com/marketing");
try {
const firstNavId = controller.beginNavigation();
await expect(
renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: firstNavId,
}),
).resolves.toBe("hard-navigate");
expect(assign).toHaveBeenCalledTimes(1);
assign.mockClear();
window.location.href = "https://example.com/settings";
const secondNavId = controller.beginNavigation();
await expect(
renderCurrentStateNavigationPayload(controller, {
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
actionType: "navigate",
createNavigationCommitEffect: () => () => {},
historyUpdateMode: "push",
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard",
{},
),
nextElements: Promise.resolve(
createResolvedElements("route:/dashboard", "/(dashboard)", null, {
"page:/dashboard": React.createElement("main", null, "dashboard"),
}),
),
operationLane: "navigation",
params: {},
pendingRouterState: null,
previousNextUrl: null,
targetHref: "https://example.com/dashboard",
navId: secondNavId,
}),
).resolves.toBe("hard-navigate");
expect(assign).toHaveBeenCalledTimes(1);
expect(assign).toHaveBeenCalledWith("https://example.com/dashboard");
} finally {
detach();
}
});
});
describe("app browser entry previousNextUrl helpers", () => {
it("stores previousNextUrl alongside existing history state", () => {
expect(
createHistoryStateWithPreviousNextUrl(
{
__vinext_scrollY: 120,
},
"/feed?tab=latest",
),
).toEqual({
__vinext_previousNextUrl: "/feed?tab=latest",
__vinext_scrollY: 120,
});
});
it("drops previousNextUrl when cleared", () => {
expect(
createHistoryStateWithPreviousNextUrl(
{
__vinext_previousNextUrl: "/feed",
__vinext_scrollY: 120,
},
null,
),
).toEqual({
__vinext_scrollY: 120,
});
});
it("reads previousNextUrl from history state", () => {
expect(
readHistoryStatePreviousNextUrl({
__vinext_previousNextUrl: "/feed?tab=latest",
}),
).toBe("/feed?tab=latest");
});
it("derives interception context from previousNextUrl pathname", () => {
expect(resolveInterceptionContextFromPreviousNextUrl("/feed?tab=latest")).toBe("/feed");
});
it("returns null when previousNextUrl is missing", () => {
expect(readHistoryStatePreviousNextUrl({})).toBeNull();
expect(resolveInterceptionContextFromPreviousNextUrl(null)).toBeNull();
});
it("stores traversal index alongside existing history state", () => {
const state = createHistoryStateWithNavigationMetadata(
{
__vinext_scrollY: 120,
},
{
previousNextUrl: "/feed?tab=latest",
traversalIndex: 4,
},
);
expect(state).toEqual({
__vinext_historyIndex: 4,
__vinext_previousNextUrl: "/feed?tab=latest",
__vinext_scrollY: 120,
});
expect(readHistoryStateTraversalIndex(state)).toBe(4);
});
it("resolves back, forward, and unknown traversal intent from history state", () => {
expect(
resolveHistoryTraversalIntent({
currentHistoryIndex: 5,
historyState: { __vinext_historyIndex: 3 },
}).direction,
).toBe("back");
expect(
resolveHistoryTraversalIntent({
currentHistoryIndex: 5,
historyState: { __vinext_historyIndex: 7 },
}).direction,
).toBe("forward");
expect(
resolveHistoryTraversalIntent({
currentHistoryIndex: 5,
historyState: {},
}),
).toEqual({
direction: "unknown",
historyState: {},
targetHistoryIndex: null,
});
expect(
resolveHistoryTraversalIntent({
currentHistoryIndex: null,
historyState: { __vinext_historyIndex: 7 },
}),
).toEqual({
direction: "unknown",
historyState: { __vinext_historyIndex: 7 },
targetHistoryIndex: 7,
});
});
it("classifies same-url payloads without treating snapshot root topology as authority", async () => {
const currentState = createState({
rootLayoutTreePath: "/(marketing)",
});
const result = await resolveAndClassifyNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
activeNavigationId: 7,
currentState,
navigationSnapshot: currentState.navigationSnapshot,
nextElements: Promise.resolve(createResolvedElements("route:/dashboard", "/(dashboard)")),
operationLane: "server-action",
renderId: 3,
startedNavigationId: 7,
targetHref: "https://example.com/dashboard?action=same-url",
type: "navigate",
});
expect(result.decision.disposition).toBe("commit");
expect(result.pending.routeId).toBe("route:/dashboard");
expect(result.pending.action.renderId).toBe(3);
expect(result.trace.entries[0]?.code).toBe(NavigationTraceTransactionCodes.visibleCommit);
expect(result.trace.entries[1]?.code).toBe(NavigationTraceReasonCodes.rootBoundaryUnknown);
expect(result.trace.entries[1]?.fields.targetHref).toBe(
"https://example.com/dashboard?action=same-url",
);
});
it("creates navigation trace entries without retaining field ownership", () => {
const fields = { activeNavigationId: 1 };
const trace = createNavigationTrace(NavigationTraceReasonCodes.commitCurrent, fields);
fields.activeNavigationId = 2;
expect(trace.entries).toEqual([
{
code: NavigationTraceReasonCodes.commitCurrent,
fields: { activeNavigationId: 1 },
},
]);
});
it("preserves only planner-approved same-layout ancestors on navigate commits", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const dashboardLayout = React.createElement("div", null, "dashboard layout");
const staleLayout = React.createElement("div", null, "stale layout");
const stalePage = React.createElement("main", null, "stale page");
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/dashboard": "_b_1_",
},
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
"layout:/": rootLayout,
"layout:/dashboard": dashboardLayout,
"layout:/stale": staleLayout,
"page:/stale": stalePage,
},
["layout:/", "layout:/dashboard"],
),
layoutFlags: {
"layout:/": "s",
"layout:/dashboard": "s",
"layout:/stale": "d",
},
layoutIds: ["layout:/", "layout:/dashboard"],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/dashboard/settings": React.createElement("main", null, "settings"),
},
layoutIds: ["layout:/", "layout:/dashboard", "layout:/dashboard/settings"],
layoutFlags: { "layout:/dashboard/settings": "d" },
rootLayoutTreePath: "/",
routeId: "route:/dashboard/settings",
});
expect(nextState.elements["layout:/"]).toBe(rootLayout);
expect(nextState.elements["layout:/dashboard"]).toBe(dashboardLayout);
expect(Object.hasOwn(nextState.elements, "layout:/stale")).toBe(false);
expect(Object.hasOwn(nextState.elements, "page:/stale")).toBe(false);
expect(nextState.layoutFlags).toEqual({
"layout:/": "s",
"layout:/dashboard": "s",
"layout:/dashboard/settings": "d",
});
expect(nextState.layoutIds).toEqual([
"layout:/",
"layout:/dashboard",
"layout:/dashboard/settings",
]);
});
it("installs fresh same-layout output on refresh commits", async () => {
const previousLayout = React.createElement("div", null, "previous layout");
const nextLayout = React.createElement("div", null, "refreshed layout");
const state = createState({
bfcacheIds: { "layout:/": "0" },
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{ "layout:/": previousLayout },
["layout:/"],
),
layoutIds: ["layout:/"],
routeId: "route:/dashboard",
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"layout:/": nextLayout,
"page:/dashboard": React.createElement("main", null, "dashboard"),
},
layoutIds: ["layout:/"],
operationLane: "refresh",
rootLayoutTreePath: "/",
routeId: "route:/dashboard",
});
expect(nextState.elements["layout:/"]).toBe(nextLayout);
});
it("installs fresh matching layouts for revalidating same-URL server actions", async () => {
const previousLayout = React.createElement("div", null, "previous layout");
const nextLayout = React.createElement("div", null, "revalidated layout");
const initialState = createState({
bfcacheIds: { "layout:/": "0" },
elements: createResolvedElements(
"route:/settings",
"/",
null,
{ "layout:/": previousLayout },
["layout:/"],
),
layoutIds: ["layout:/"],
rootLayoutTreePath: "/",
routeId: "route:/settings",
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {}),
});
const { controller, detach, stateRef } = createControllerHarness(initialState);
stubWindow("https://example.com/settings");
try {
await controller.commitSameUrlNavigatePayload(
Promise.resolve(
createResolvedElements(
"route:/settings",
"/",
null,
{
"layout:/": nextLayout,
"page:/settings": React.createElement("main", null, "settings"),
},
["layout:/"],
),
),
stateRef.current.navigationSnapshot,
undefined,
stateRef.current,
{ revalidation: "staticAndDynamic" },
);
expect(stateRef.current.activeOperation).toMatchObject({ lane: "refresh" });
expect(stateRef.current.elements["layout:/"]).toBe(nextLayout);
} finally {
detach();
}
});
it("installs fresh dynamic layout output when its bound segment identity changes", async () => {
const previousLayout = React.createElement("div", null, "hello-world layout");
const nextLayout = React.createElement("div", null, "getting-started layout");
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/blog/[slug]": "_b_2_",
},
elements: createResolvedElements(
"route:/blog/[slug]",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/blog/[slug]": previousLayout,
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
"layout:/": "/",
"layout:/blog/[slug]": "/blog/[slug]?slug=hello-world",
},
},
["layout:/", "layout:/blog/[slug]"],
),
layoutIds: ["layout:/", "layout:/blog/[slug]"],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/blog/hello-world",
{ slug: "hello-world" },
),
routeId: "route:/blog/[slug]",
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"layout:/blog/[slug]": nextLayout,
"page:/blog/[slug]": React.createElement("main", null, "getting-started"),
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
"layout:/": "/",
"layout:/blog/[slug]": "/blog/[slug]?slug=getting-started",
"page:/blog/[slug]": "/blog/[slug]?slug=getting-started",
},
},
layoutIds: ["layout:/", "layout:/blog/[slug]"],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/blog/getting-started",
{ slug: "getting-started" },
),
rootLayoutTreePath: "/",
routeId: "route:/blog/[slug]",
targetHref: "https://example.com/blog/getting-started",
});
expect(nextState.elements["layout:/blog/[slug]"]).toBe(nextLayout);
expect(nextState.bfcacheIds["layout:/blog/[slug]"]).not.toBe("_b_2_");
});
it("does not preserve a previous default slot when its dynamic owner identity changes", async () => {
const layoutId = "layout:/blog/[slug]";
const slotId = AppElementsWire.encodeSlotId("sidebar", "/blog/[slug]");
const previousSlot = React.createElement("aside", null, "hello-world sidebar");
const nextSlot = React.createElement("aside", null, "getting-started sidebar");
const state = createState({
bfcacheIds: { [layoutId]: "_b_3_" },
elements: createResolvedElements(
"route:/blog/[slug]",
"/",
null,
{
[layoutId]: React.createElement("div", null, "hello-world layout"),
[slotId]: previousSlot,
},
["layout:/", layoutId],
[{ ownerLayoutId: layoutId, slotId, state: "default" }],
),
layoutIds: ["layout:/", layoutId],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/blog/hello-world",
{ slug: "hello-world" },
),
routeId: "route:/blog/[slug]",
slotBindings: [{ ownerLayoutId: layoutId, slotId, state: "default" }],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
[layoutId]: React.createElement("div", null, "getting-started layout"),
[slotId]: nextSlot,
},
layoutIds: ["layout:/", layoutId],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/blog/getting-started",
{ slug: "getting-started" },
),
rootLayoutTreePath: "/",
routeId: "route:/blog/[slug]",
slotBindings: [{ ownerLayoutId: layoutId, slotId, state: "default" }],
targetHref: "https://example.com/blog/getting-started",
});
expect(nextState.elements[slotId]).toBe(nextSlot);
expect(nextState.slotBindings).toEqual([{ ownerLayoutId: layoutId, slotId, state: "default" }]);
});
it("does not preserve same-layout ancestors when root identity is unknown", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const state = createState({
elements: createResolvedElements("route:/dashboard", "/", null, { "layout:/": rootLayout }, [
"layout:/",
]),
layoutIds: ["layout:/"],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/legacy": React.createElement("main", null, "legacy"),
},
layoutIds: [],
rootLayoutTreePath: null,
routeId: "route:/legacy",
});
expect(Object.hasOwn(nextState.elements, "layout:/")).toBe(false);
expect(nextState.layoutIds).toEqual([]);
});
it("preserves explicitly skipped retained layouts on approved navigate commits", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const staleLayout = React.createElement("div", null, "stale layout");
const currentState = createState({
bfcacheIds: {
"layout:/": "0",
},
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
"layout:/": rootLayout,
"layout:/stale": staleLayout,
},
["layout:/"],
),
layoutFlags: {
"layout:/": "s",
"layout:/stale": "s",
},
layoutIds: ["layout:/"],
});
const pending = await createPendingNavigationCommit({
currentState,
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {}),
nextElements: Promise.resolve(
createResolvedElements(
"route:/settings",
"/",
null,
{
[APP_LAYOUT_FLAGS_KEY]: {},
[APP_SKIPPED_LAYOUT_IDS_KEY]: ["layout:/", "layout:/stale"],
"page:/settings": React.createElement("main", null, "settings"),
},
["layout:/"],
),
),
operationLane: "navigation",
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
renderId: 1,
type: "navigate",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState,
pending,
routeManifest: null,
startedNavigationId: 1,
targetHref: "https://example.com/settings",
});
expect(approval.approvedCommit).not.toBeNull();
if (approval.approvedCommit === null) return;
expect(approval.decision.preserveElementIds).toEqual(["layout:/"]);
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(nextState.elements["layout:/"]).toBe(rootLayout);
expect(Object.hasOwn(nextState.elements, "layout:/stale")).toBe(false);
expect(nextState.layoutFlags).toEqual({
"layout:/": "s",
});
});
it("does not preserve skipped layouts when target bfcache ids mismatch", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const currentState = createState({
bfcacheIds: {
"layout:/": "0",
},
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
"layout:/": rootLayout,
},
["layout:/"],
),
layoutIds: ["layout:/"],
});
const pending = await createPendingNavigationCommit({
currentState,
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {}),
nextElements: Promise.resolve(
createResolvedElements(
"route:/settings",
"/",
null,
{
[APP_SKIPPED_LAYOUT_IDS_KEY]: ["layout:/"],
"page:/settings": React.createElement("main", null, "settings"),
},
["layout:/"],
),
),
operationLane: "navigation",
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
renderId: 1,
type: "navigate",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState,
pending,
routeManifest: null,
startedNavigationId: 1,
targetHref: "https://example.com/settings",
});
expect(approval.approvedCommit).not.toBeNull();
if (approval.approvedCommit === null) return;
const mismatchedCommit = {
...approval.approvedCommit,
action: {
...approval.approvedCommit.action,
bfcacheIds: {
...approval.approvedCommit.action.bfcacheIds,
"layout:/": "_b_stale_",
},
},
};
const nextState = applyApprovedVisibleCommit(currentState, mismatchedCommit);
expect(Object.hasOwn(nextState.elements, "layout:/")).toBe(false);
expect(nextState.elements["page:/settings"]).toBeDefined();
expect(nextState.bfcacheIds["layout:/"]).toBe("_b_stale_");
});
it("does not preserve skipped layouts when current bfcache ids are stale", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const currentState = createState({
bfcacheIds: {
"layout:/": "_b_4_",
},
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
"layout:/": rootLayout,
},
["layout:/"],
),
layoutFlags: {
"layout:/": "s",
},
layoutIds: ["layout:/"],
});
const pending = await createPendingNavigationCommit({
currentState,
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/settings", {}),
nextElements: Promise.resolve(
createResolvedElements(
"route:/settings",
"/",
null,
{
[APP_LAYOUT_FLAGS_KEY]: {},
[APP_SKIPPED_LAYOUT_IDS_KEY]: ["layout:/"],
"page:/settings": React.createElement("main", null, "settings"),
},
["layout:/"],
),
),
operationLane: "traverse",
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
renderId: 1,
reuseCurrentBfcacheIds: false,
type: "traverse",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState,
pending,
routeManifest: null,
startedNavigationId: 1,
targetHref: "https://example.com/settings",
});
expect(approval.approvedCommit).not.toBeNull();
if (approval.approvedCommit === null) return;
expect(approval.decision.preserveElementIds).toEqual(["layout:/"]);
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(Object.hasOwn(nextState.elements, "layout:/")).toBe(false);
expect(nextState.elements["page:/settings"]).toBeDefined();
expect(nextState.layoutFlags).toEqual({});
expect(nextState.bfcacheIds["layout:/"]).toMatch(/^_b_\d+_$/);
expect(nextState.bfcacheIds["layout:/"]).not.toBe("_b_4_");
});
it("preserves the default parallel slot owned by a skipped slot-owning layout", async () => {
const rootLayout = React.createElement("div", null, "root layout");
const dashboardLayout = React.createElement("div", null, "dashboard layout");
const modalSlot = React.createElement("div", null, "modal");
const modalSlotId = AppElementsWire.encodeSlotId("modal", "/dashboard");
const currentModalBinding = {
ownerLayoutId: "layout:/dashboard",
slotId: modalSlotId,
state: "active",
} satisfies AppElementsSlotBinding;
const currentState = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/dashboard": "_b_4_",
[modalSlotId]: "_b_5_",
},
elements: createResolvedElements(
"route:/dashboard",
"/",
null,
{
"layout:/": rootLayout,
"layout:/dashboard": dashboardLayout,
[modalSlotId]: modalSlot,
},
["layout:/", "layout:/dashboard"],
[currentModalBinding],
),
layoutFlags: {
"layout:/": "s",
"layout:/dashboard": "s",
},
layoutIds: ["layout:/", "layout:/dashboard"],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/dashboard", {}),
routeId: "route:/dashboard",
slotBindings: [currentModalBinding],
});
// Sibling navigation: the server proves layout:/dashboard reusable and omits
// it from the payload, and the modal slot resolves to its default (no active
// content) for the target route. In the topology-unknown path the planner
// preserves neither the layout nor its slot, so the skip merge must restore
// both — otherwise the retained layout commits with a missing slot.
const pending = await createPendingNavigationCommit({
currentState,
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/dashboard/settings",
{},
),
nextElements: Promise.resolve(
createResolvedElements(
"route:/dashboard/settings",
"/",
null,
{
[APP_LAYOUT_FLAGS_KEY]: {},
[APP_SKIPPED_LAYOUT_IDS_KEY]: ["layout:/dashboard"],
"page:/dashboard/settings": React.createElement("main", null, "settings"),
},
["layout:/", "layout:/dashboard"],
[
{
ownerLayoutId: "layout:/dashboard",
slotId: modalSlotId,
state: "default",
},
],
),
),
operationLane: "navigation",
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
renderId: 1,
type: "navigate",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState,
pending,
routeManifest: null,
startedNavigationId: 1,
targetHref: "https://example.com/dashboard/settings",
});
expect(approval.approvedCommit).not.toBeNull();
if (approval.approvedCommit === null) return;
expect(approval.decision.disposition).toBe("commit");
if (approval.decision.disposition !== "commit") return;
expect(approval.decision.preserveElementIds).toContain("layout:/dashboard");
expect(approval.decision.preservePreviousSlotIds).toContain(modalSlotId);
const nextState = applyApprovedVisibleCommit(currentState, approval.approvedCommit);
expect(nextState.elements["layout:/dashboard"]).toBe(dashboardLayout);
expect(nextState.elements[modalSlotId]).toBe(modalSlot);
expect(
AppElementsWire.readMetadata(nextState.elements).bfcacheSegmentIdentities[modalSlotId],
).toBe(`test-identity:${modalSlotId}`);
expect(nextState.bfcacheIds[modalSlotId]).toBe("_b_5_");
const followingElements = createResolvedElements(
"route:/dashboard/profile",
"/",
null,
{
"layout:/": rootLayout,
"layout:/dashboard": dashboardLayout,
[modalSlotId]: modalSlot,
"page:/dashboard/profile": React.createElement("main", null, "profile"),
},
["layout:/", "layout:/dashboard"],
[currentModalBinding],
);
const followingBfcacheIds = createNextBfcacheIdMap({
current: nextState.bfcacheIds,
currentElements: nextState.elements,
elements: followingElements,
});
expect(followingBfcacheIds[modalSlotId]).toBe("_b_5_");
});
it("clears stale parallel slots on approved traverse commits", async () => {
const state = createState({
elements: createResolvedElements("route:/feed", "/", null, {
"slot:modal:/feed": React.createElement("div", null, "modal"),
}),
});
const nextState = await applyApprovedTestCommit(state, {
previousNextUrl: null,
rootLayoutTreePath: "/",
routeId: "route:/feed",
type: "traverse",
});
expect(Object.hasOwn(nextState.elements, "slot:modal:/feed")).toBe(false);
});
it("does not approve mounted parallel slots on approved traverse commits", async () => {
const feedLayout = React.createElement("div", null, "feed layout");
const mountedSlot = React.createElement("div", null, "modal");
const modalSlotBinding = {
ownerLayoutId: "layout:/feed",
slotId: "slot:modal:/feed",
state: "active",
} satisfies AppElementsSlotBinding;
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
"slot:modal:/feed": "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": feedLayout,
"slot:modal:/feed": mountedSlot,
},
["layout:/", "layout:/feed"],
[modalSlotBinding],
),
layoutIds: ["layout:/", "layout:/feed"],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/feed", {}),
routeId: "route:/feed",
slotBindings: [modalSlotBinding],
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: state,
nextElements: Promise.resolve(
createResolvedElements(
"route:/feed/comments",
"/",
null,
{
"page:/feed/comments": React.createElement("main", null, "comments"),
},
["layout:/", "layout:/feed", "layout:/feed/comments"],
[
{
ownerLayoutId: "layout:/feed",
slotId: "slot:modal:/feed",
state: "default",
},
],
),
),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/feed/comments",
{},
),
operationLane: "traverse",
previousNextUrl: null,
renderId: 1,
type: "traverse",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState: state,
pending,
routeManifest: createRouteManifestForPendingCommit(state, pending),
startedNavigationId: 1,
targetHref: "https://example.com/feed/comments",
});
expect(approval.decision.disposition).toBe("commit");
if (approval.decision.disposition !== "commit") {
throw new Error("Expected visible commit approval");
}
expect(approval.decision.preserveElementIds).toEqual(["layout:/", "layout:/feed"]);
expect(approval.decision.preservePreviousSlotIds).toEqual([]);
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
const nextState = applyApprovedVisibleCommit(state, approval.approvedCommit);
expect(nextState.elements["layout:/feed"]).toBe(feedLayout);
expect(Object.hasOwn(nextState.elements, "slot:modal:/feed")).toBe(false);
});
it("traverse: preserves bfcacheIds for planner-approved layout elements", async () => {
const modalSlotId = AppElementsWire.encodeSlotId("modal", "/feed");
const feedLayout = React.createElement("div", null, "feed layout");
const modalSlotBinding = {
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "active",
} satisfies AppElementsSlotBinding;
const currentIdentities = {
"layout:/": "/",
"layout:/feed": "/feed",
};
const nextIdentities = {
...currentIdentities,
"page:/feed/comments": "/feed/comments",
};
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
[modalSlotId]: "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": feedLayout,
[modalSlotId]: React.createElement("div", null, "modal"),
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: currentIdentities,
},
["layout:/", "layout:/feed"],
[modalSlotBinding],
),
layoutIds: ["layout:/", "layout:/feed"],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/feed", {}),
routeId: "route:/feed",
slotBindings: [modalSlotBinding],
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState: state,
nextElements: Promise.resolve(
createResolvedElements(
"route:/feed/comments",
"/",
null,
{
"page:/feed/comments": React.createElement("main", null, "comments"),
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: nextIdentities,
},
["layout:/", "layout:/feed", "layout:/feed/comments"],
[
{
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "default",
},
],
),
),
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/feed/comments",
{},
),
operationLane: "traverse",
previousNextUrl: null,
renderId: 1,
type: "traverse",
});
const approval = approvePendingNavigationCommit({
activeNavigationId: 1,
currentState: state,
pending,
routeManifest: createRouteManifestForPendingCommit(state, pending),
startedNavigationId: 1,
targetHref: "https://example.com/feed/comments",
});
expect(approval.decision.disposition).toBe("commit");
if (approval.decision.disposition !== "commit") {
throw new Error("Expected visible commit approval");
}
expect(approval.decision.preserveElementIds).toEqual(["layout:/", "layout:/feed"]);
expect(approval.decision.preservePreviousSlotIds).toEqual([]);
if (approval.approvedCommit === null) {
throw new Error("Expected approved visible commit");
}
expect(approval.approvedCommit.action.bfcacheIds["layout:/feed"]).toBe("_b_4_");
// Add a stale slot id to verify the merged element set bounds the map.
const reducerBfcacheIdProbe = {
...approval.approvedCommit.action.bfcacheIds,
[modalSlotId]: "_b_5_",
};
const commitWithoutPreservedLayoutBfcacheId = {
...approval.approvedCommit,
action: {
...approval.approvedCommit.action,
bfcacheIds: reducerBfcacheIdProbe,
},
};
expect(commitWithoutPreservedLayoutBfcacheId.action.bfcacheIds["layout:/feed"]).toBe("_b_4_");
expect(commitWithoutPreservedLayoutBfcacheId.action.bfcacheIds[modalSlotId]).toBe("_b_5_");
const nextState = applyApprovedVisibleCommit(state, commitWithoutPreservedLayoutBfcacheId);
expect(nextState.elements["layout:/feed"]).toBe(feedLayout);
expect(nextState.bfcacheIds["layout:/"]).toBe("0");
expect(nextState.bfcacheIds["layout:/feed"]).toBe("_b_4_");
expect(Object.hasOwn(nextState.elements, modalSlotId)).toBe(false);
expect(Object.hasOwn(nextState.bfcacheIds, modalSlotId)).toBe(false);
});
it("preserves planner-approved default parallel slots on approved navigate commits", async () => {
const mountedSlot = React.createElement("div", null, "modal");
const modalSlotBinding = {
ownerLayoutId: "layout:/feed",
slotId: "slot:modal:/feed",
state: "active",
} satisfies AppElementsSlotBinding;
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
"slot:modal:/feed": "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": React.createElement("div", null, "feed layout"),
"slot:modal:/feed": mountedSlot,
},
["layout:/", "layout:/feed"],
[modalSlotBinding],
),
layoutIds: ["layout:/", "layout:/feed"],
slotBindings: [modalSlotBinding],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/feed/comments": React.createElement("main", null, "comments"),
},
layoutIds: ["layout:/", "layout:/feed", "layout:/feed/comments"],
rootLayoutTreePath: "/",
routeId: "route:/feed/comments",
slotBindings: [
{
ownerLayoutId: "layout:/feed",
slotId: "slot:modal:/feed",
state: "default",
},
],
});
expect(Object.hasOwn(nextState.elements, "slot:modal:/feed")).toBe(true);
expect(nextState.elements["slot:modal:/feed"]).toBe(mountedSlot);
expect(nextState.slotBindings).toEqual([modalSlotBinding]);
});
it("preserves bfcache ids for planner-approved default parallel slots", async () => {
const modalSlotId = AppElementsWire.encodeSlotId("modal", "/feed");
const mountedSlot = React.createElement("div", null, "modal");
const modalSlotBinding = {
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "active",
} satisfies AppElementsSlotBinding;
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
[modalSlotId]: "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": React.createElement("div", null, "feed layout"),
[modalSlotId]: mountedSlot,
},
["layout:/", "layout:/feed"],
[modalSlotBinding],
),
layoutIds: ["layout:/", "layout:/feed"],
slotBindings: [modalSlotBinding],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/feed/comments": React.createElement("main", null, "comments"),
},
layoutIds: ["layout:/", "layout:/feed", "layout:/feed/comments"],
rootLayoutTreePath: "/",
routeId: "route:/feed/comments",
slotBindings: [
{
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "default",
},
],
});
expect(nextState.elements[modalSlotId]).toBe(mountedSlot);
expect(nextState.bfcacheIds[modalSlotId]).toBe("_b_5_");
});
it("keeps previous slot binding proof when the target marks a preserved slot unmatched", async () => {
const mountedSlot = React.createElement("div", null, "modal");
const modalSlotId = "slot:modal:/feed";
const currentIdentities = {
"layout:/": "identity:layout:/",
"layout:/feed": "identity:layout:/feed",
[modalSlotId]: "identity:modal:active",
};
const modalSlotBinding = {
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "active",
} satisfies AppElementsSlotBinding;
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
"slot:modal:/feed": "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: currentIdentities,
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": React.createElement("div", null, "feed layout"),
[modalSlotId]: mountedSlot,
},
["layout:/", "layout:/feed"],
[modalSlotBinding],
),
layoutIds: ["layout:/", "layout:/feed"],
slotBindings: [modalSlotBinding],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
...currentIdentities,
[modalSlotId]: "identity:modal:unmatched",
"page:/feed/comments": "identity:page:/feed/comments",
},
"page:/feed/comments": React.createElement("main", null, "comments"),
[modalSlotId]: UNMATCHED_SLOT,
},
layoutIds: ["layout:/", "layout:/feed", "layout:/feed/comments"],
rootLayoutTreePath: "/",
routeId: "route:/feed/comments",
slotBindings: [
{
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "unmatched",
},
],
});
expect(nextState.elements[modalSlotId]).toBe(mountedSlot);
expect(
AppElementsWire.readMetadata(nextState.elements).bfcacheSegmentIdentities[modalSlotId],
).toBe("identity:modal:active");
expect(nextState.bfcacheIds[modalSlotId]).toBe("_b_5_");
expect(nextState.slotBindings).toEqual([modalSlotBinding]);
});
it("remints a preserved slot when its previous payload had no identity proof", async () => {
const modalSlotId = "slot:modal:/feed";
const mountedSlot = React.createElement("div", null, "modal");
const layoutIdentities = {
"layout:/": "identity:layout:/",
"layout:/feed": "identity:layout:/feed",
};
const modalSlotBinding = {
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "active",
} satisfies AppElementsSlotBinding;
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
[modalSlotId]: "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: layoutIdentities,
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": React.createElement("div", null, "feed layout"),
[modalSlotId]: mountedSlot,
},
["layout:/", "layout:/feed"],
[modalSlotBinding],
),
layoutIds: ["layout:/", "layout:/feed"],
slotBindings: [modalSlotBinding],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
...layoutIdentities,
[modalSlotId]: "identity:modal:unmatched",
"page:/feed/comments": "identity:page:/feed/comments",
},
"page:/feed/comments": React.createElement("main", null, "comments"),
[modalSlotId]: UNMATCHED_SLOT,
},
layoutIds: ["layout:/", "layout:/feed", "layout:/feed/comments"],
rootLayoutTreePath: "/",
routeId: "route:/feed/comments",
slotBindings: [
{
ownerLayoutId: "layout:/feed",
slotId: modalSlotId,
state: "unmatched",
},
],
});
expect(nextState.elements[modalSlotId]).toBe(mountedSlot);
expect(
AppElementsWire.readMetadata(nextState.elements).bfcacheSegmentIdentities[modalSlotId],
).toBeUndefined();
expect(nextState.bfcacheIds[modalSlotId]).toMatch(/^_b_\d+_$/);
expect(nextState.bfcacheIds[modalSlotId]).not.toBe("_b_5_");
});
it("does not infer default slot preservation from previous wire entries", async () => {
const state = createState({
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": React.createElement("div", null, "feed layout"),
"slot:modal:/feed": React.createElement("div", null, "modal"),
},
["layout:/", "layout:/feed"],
),
layoutIds: ["layout:/", "layout:/feed"],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/feed/comments": React.createElement("main", null, "comments"),
},
layoutIds: ["layout:/", "layout:/feed", "layout:/feed/comments"],
rootLayoutTreePath: "/",
routeId: "route:/feed/comments",
slotBindings: [
{
ownerLayoutId: "layout:/feed",
slotId: "slot:modal:/feed",
state: "default",
},
],
});
expect(Object.hasOwn(nextState.elements, "slot:modal:/feed")).toBe(false);
});
it("does not preserve absent parallel slots when their owner layout is not approved", async () => {
const modalSlotId = AppElementsWire.encodeSlotId("modal", "/feed");
const state = createState({
bfcacheIds: {
"layout:/": "0",
"layout:/feed": "_b_4_",
[modalSlotId]: "_b_5_",
},
elements: createResolvedElements(
"route:/feed",
"/",
null,
{
"layout:/": React.createElement("div", null, "root layout"),
"layout:/feed": React.createElement("div", null, "feed layout"),
[modalSlotId]: React.createElement("div", null, "modal"),
},
["layout:/", "layout:/feed"],
),
layoutIds: ["layout:/", "layout:/feed"],
});
const nextState = await applyApprovedTestCommit(state, {
extraEntries: {
"page:/settings": React.createElement("main", null, "settings"),
},
layoutIds: ["layout:/", "layout:/settings"],
rootLayoutTreePath: "/",
routeId: "route:/settings",
});
expect(Object.hasOwn(nextState.elements, "slot:modal:/feed")).toBe(false);
expect(Object.hasOwn(nextState.bfcacheIds, modalSlotId)).toBe(false);
});
});
describe("app browser entry bfcacheId helpers", () => {
const rootLayoutId = AppElementsWire.encodeLayoutId("/");
const groupLayoutId = AppElementsWire.encodeLayoutId("/[group]");
const nestedGroupLayoutId = AppElementsWire.encodeLayoutId(
"/nextjs-compat/use-router-bfcache-id/[group]",
);
const pageX1Id = AppElementsWire.encodePageId("/x/1", null);
const pageX2Id = AppElementsWire.encodePageId("/x/2", null);
const pageY1Id = AppElementsWire.encodePageId("/y/1", null);
function createDefaultBfcacheSegmentIdentities(pageId: string): Record<string, string> {
return {
[rootLayoutId]: `identity:${rootLayoutId}`,
[groupLayoutId]: `identity:${groupLayoutId}`,
[pageId]: `identity:${pageId}`,
};
}
function createBfcacheElements(
pageId: string,
options: {
graphVersion?: string | null;
bfcacheSegmentIdentities?: Record<string, string> | null;
} = {},
): AppElements {
const extraEntries: Record<string, unknown> = {};
if (options.bfcacheSegmentIdentities !== null) {
extraEntries[APP_BFCACHE_SEGMENT_IDENTITIES_KEY] =
options.bfcacheSegmentIdentities ?? createDefaultBfcacheSegmentIdentities(pageId);
} else {
extraEntries[APP_BFCACHE_SEGMENT_IDENTITIES_KEY] = undefined;
}
if (options.graphVersion !== undefined) {
extraEntries[APP_ARTIFACT_COMPATIBILITY_KEY] = createArtifactCompatibilityEnvelope({
graphVersion: options.graphVersion,
});
}
return createResolvedElements(
`route:${pageId.slice("page:".length)}`,
"/",
null,
{
[rootLayoutId]: React.createElement("div", null),
[groupLayoutId]: React.createElement("div", null),
[pageId]: React.createElement("main", null),
...extraEntries,
},
[rootLayoutId, groupLayoutId],
);
}
it("initializes every visible segment with the hydration placeholder", () => {
expect(createInitialBfcacheIdMap(createBfcacheElements(pageX1Id))).toEqual({
[rootLayoutId]: "0",
[groupLayoutId]: "0",
[pageX1Id]: "0",
});
});
it("builds initial bfcache maps from shared App Elements metadata", () => {
const elements = createBfcacheElements(pageX1Id);
const metadata = AppElementsWire.readMetadata(elements);
const maps = createInitialBfcacheMaps({
elements,
metadata,
});
expect(maps.bfcacheIds).toEqual(createInitialBfcacheIdMap(elements));
expect(maps.identities).toEqual(
createBfcacheSegmentIdentityMap({
elements,
}),
);
});
it("writes only history-readable bfcache segment ids", () => {
const routeId = AppElementsWire.encodeRouteId("/x/1", null);
const elements = createResolvedElements(routeId, "/", null, {
[routeId]: React.createElement("main", null),
[rootLayoutId]: React.createElement("div", null),
[pageX1Id]: React.createElement("main", null),
});
const metadata = AppElementsWire.readMetadata(elements);
const maps = createInitialBfcacheMaps({
elements,
metadata,
});
const state = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: maps.bfcacheIds,
bfcacheVersion: 1,
previousNextUrl: null,
});
expect(maps.bfcacheIds).toEqual({
[rootLayoutId]: "0",
[pageX1Id]: "0",
});
expect(readHistoryStateBfcacheIds(state)).toEqual(maps.bfcacheIds);
});
it("preserves segment ids when carried BFCache identities match", () => {
const dynamicPageId = AppElementsWire.encodePageId("/page/[n]", null);
const bfcacheSegmentIdentities = {
[rootLayoutId]: "identity:root:page-1",
[groupLayoutId]: "identity:group:page-1",
[dynamicPageId]: "identity:page-1",
};
const next = createNextBfcacheIdMap({
current: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[dynamicPageId]: "_b_5_",
},
currentElements: createBfcacheElements(dynamicPageId, { bfcacheSegmentIdentities }),
elements: createBfcacheElements(dynamicPageId, { bfcacheSegmentIdentities }),
});
expect(next[dynamicPageId]).toBe("_b_5_");
});
it("mints fresh segment ids when carried BFCache identities are absent", () => {
const dynamicPageId = AppElementsWire.encodePageId("/page/[n]", null);
const next = createNextBfcacheIdMap({
current: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[dynamicPageId]: "_b_5_",
},
currentElements: createBfcacheElements(dynamicPageId, { bfcacheSegmentIdentities: null }),
elements: createBfcacheElements(dynamicPageId, { bfcacheSegmentIdentities: null }),
});
expect(next[rootLayoutId]).toMatch(/^_b_\d+_$/);
expect(next[rootLayoutId]).not.toBe("0");
expect(next[groupLayoutId]).toMatch(/^_b_\d+_$/);
expect(next[groupLayoutId]).not.toBe("_b_4_");
expect(next[dynamicPageId]).toMatch(/^_b_\d+_$/);
expect(next[dynamicPageId]).not.toBe("_b_5_");
});
it("does not restore history ids without destination identity proof", () => {
const dynamicPageId = AppElementsWire.encodePageId("/page/[n]", null);
const next = createNextBfcacheIdMap({
current: { [dynamicPageId]: "_b_5_" },
currentElements: createBfcacheElements(dynamicPageId),
elements: createBfcacheElements(dynamicPageId, { bfcacheSegmentIdentities: null }),
restored: { [dynamicPageId]: "_b_9_" },
});
expect(next[dynamicPageId]).toMatch(/^_b_\d+_$/);
expect(next[dynamicPageId]).not.toBe("_b_9_");
});
it("mints a fresh segment id when a carried BFCache identity changes", () => {
const dynamicPageId = AppElementsWire.encodePageId("/page/[n]", null);
const currentIdentities = {
[rootLayoutId]: "identity:root:page-1",
[groupLayoutId]: "identity:group:page-1",
[dynamicPageId]: "identity:page-1",
};
const nextIdentities = {
[rootLayoutId]: "identity:root:page-1",
[groupLayoutId]: "identity:group:page-1",
[dynamicPageId]: "identity:page-2",
};
const next = createNextBfcacheIdMap({
current: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[dynamicPageId]: "_b_5_",
},
currentElements: createBfcacheElements(dynamicPageId, {
bfcacheSegmentIdentities: currentIdentities,
}),
elements: createBfcacheElements(dynamicPageId, {
bfcacheSegmentIdentities: nextIdentities,
}),
});
expect(next[groupLayoutId]).toBe("_b_4_");
expect(next[dynamicPageId]).toMatch(/^_b_\d+_$/);
expect(next[dynamicPageId]).not.toBe("_b_5_");
});
it("preserves segment ids when only unrelated artifact metadata changes", () => {
const dynamicPageId = AppElementsWire.encodePageId("/page/[n]", null);
const bfcacheSegmentIdentities = {
[rootLayoutId]: "identity:root:page-1",
[groupLayoutId]: "identity:group:page-1",
[dynamicPageId]: "identity:page-1",
};
const next = createNextBfcacheIdMap({
current: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[dynamicPageId]: "_b_5_",
},
currentElements: createBfcacheElements(dynamicPageId, {
graphVersion: "graph:one",
bfcacheSegmentIdentities,
}),
elements: createBfcacheElements(dynamicPageId, {
graphVersion: "graph:two",
bfcacheSegmentIdentities,
}),
});
expect(next[rootLayoutId]).toBe("0");
expect(next[groupLayoutId]).toBe("_b_4_");
expect(next[dynamicPageId]).toBe("_b_5_");
});
it("does not seed hydration bfcache ids from previously minted ids", () => {
// Next generates the public "_b_0_" hydration sentinel from an internal
// zero cache node on both SSR and initial client hydration. Minted ids
// (and persisted history maps) are restored only for explicit back/forward
// traversals, never folded into the initial map.
//
// Mint real "_b_N_" ids for these segments first, then prove that building
// the initial map for the same elements ignores them and resets every
// segment to the raw "0" hydration sentinel. This would fail if
// createInitialBfcacheIdMap ever started seeding from prior/minted state.
const minted = createNextBfcacheIdMap({
current: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[pageX2Id]: "_b_5_",
},
currentElements: createBfcacheElements(pageX2Id),
elements: createBfcacheElements(pageX1Id),
});
expect(minted[pageX1Id]).toMatch(/^_b_\d+_$/);
const initial = createInitialBfcacheIdMap(createBfcacheElements(pageX1Id));
expect(initial).toEqual({
[rootLayoutId]: "0",
[groupLayoutId]: "0",
[pageX1Id]: "0",
});
// No segment carries a minted "_b_N_" id into the initial map.
expect(Object.values(initial).every((value) => value === "0")).toBe(true);
});
it("preserves shared segment ids and mints ids for fresh segments", () => {
const current = {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[pageX1Id]: "_b_5_",
};
const next = createNextBfcacheIdMap({
current,
currentElements: createBfcacheElements(pageX1Id),
elements: createBfcacheElements(pageX2Id),
});
expect(next[rootLayoutId]).toBe("0");
expect(next[groupLayoutId]).toBe("_b_4_");
expect(next[pageX1Id]).toBeUndefined();
expect(next[pageX2Id]).toMatch(/^_b_\d+_$/);
expect(next[pageX2Id]).not.toBe("_b_5_");
});
it("mints a fresh layout id when its carried identity changes", () => {
const current = {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[pageX1Id]: "_b_5_",
};
const next = createNextBfcacheIdMap({
current,
currentElements: createBfcacheElements(pageX1Id, {
bfcacheSegmentIdentities: {
[rootLayoutId]: "identity:root",
[groupLayoutId]: "identity:group:x",
[pageX1Id]: "identity:page:x",
},
}),
elements: createBfcacheElements(pageY1Id, {
bfcacheSegmentIdentities: {
[rootLayoutId]: "identity:root",
[groupLayoutId]: "identity:group:y",
[pageY1Id]: "identity:page:y",
},
}),
});
expect(next[rootLayoutId]).toBe("0");
expect(next[groupLayoutId]).toMatch(/^_b_\d+_$/);
expect(next[groupLayoutId]).not.toBe("_b_4_");
});
it("mints a fresh nested layout id when a dynamic layout segment changes", () => {
const current = {
[rootLayoutId]: "0",
[nestedGroupLayoutId]: "0",
[pageX1Id]: "0",
};
const next = createNextBfcacheIdMap({
current,
currentElements: createResolvedElements(
"route:/nextjs-compat/use-router-bfcache-id/x/1",
"/",
null,
{
[pageX1Id]: React.createElement("main", null),
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
[rootLayoutId]: "identity:root:x",
[nestedGroupLayoutId]: "identity:nested-layout:x",
[pageX1Id]: "identity:page:x",
},
},
[rootLayoutId, nestedGroupLayoutId],
),
elements: createResolvedElements(
"route:/nextjs-compat/use-router-bfcache-id/y/1",
"/",
null,
{
[pageY1Id]: React.createElement("main", null),
[APP_BFCACHE_SEGMENT_IDENTITIES_KEY]: {
[rootLayoutId]: "identity:root:y",
[nestedGroupLayoutId]: "identity:nested-layout:y",
[pageY1Id]: "identity:page:y",
},
},
[rootLayoutId, nestedGroupLayoutId],
),
});
expect(next[nestedGroupLayoutId]).toMatch(/^_b_\d+_$/);
expect(next[nestedGroupLayoutId]).not.toBe("0");
});
it("serializes and restores bfcache ids through history state", () => {
const state = createHistoryStateWithNavigationMetadata(
{ __vinext_scrollY: 120 },
{
bfcacheIds: { [pageX1Id]: "_b_9_" },
bfcacheVersion: 3,
previousNextUrl: "/feed",
},
);
expect(state).toEqual({
__vinext_bfcacheIds: { [pageX1Id]: "_b_9_" },
__vinext_bfcacheVersion: 3,
__vinext_previousNextUrl: "/feed",
__vinext_scrollY: 120,
});
expect(readHistoryStateBfcacheIds(state)).toEqual({ [pageX1Id]: "_b_9_" });
expect(readHistoryStateBfcacheVersion(state)).toBe(3);
});
it("drops bfcache version metadata when bfcache ids are cleared", () => {
const state = createHistoryStateWithNavigationMetadata(
{
__vinext_bfcacheIds: { [pageX1Id]: "_b_9_" },
__vinext_bfcacheVersion: 3,
custom: "preserve",
},
{
bfcacheIds: {},
bfcacheVersion: 4,
previousNextUrl: "/feed",
},
);
expect(state).toEqual({
__vinext_previousNextUrl: "/feed",
custom: "preserve",
});
expect(readHistoryStateBfcacheIds(state)).toBeNull();
expect(readHistoryStateBfcacheVersion(state)).toBeNull();
});
it("treats a matching stored bfcache version as current", () => {
const state = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: { [pageX1Id]: "_b_9_" },
bfcacheVersion: 3,
previousNextUrl: null,
});
expect(isHistoryStateBfcacheVersionCurrent(state, 3)).toBe(true);
expect(isHistoryStateBfcacheVersionCurrent(state, 4)).toBe(false);
});
it("rejects a missing bfcache version even when the current version is 0", () => {
// Regression guard: a history entry carrying bfcache ids but no version key
// (older build / external pushState) must NOT pass the document-scoped gate
// on a fresh document whose current version is 0. Coercing the missing
// version to 0 would let stale ids be restored across documents.
const unversioned = { __vinext_bfcacheIds: { [pageX1Id]: "_b_9_" } };
expect(readHistoryStateBfcacheVersion(unversioned)).toBeNull();
expect(isHistoryStateBfcacheVersionCurrent(unversioned, 0)).toBe(false);
expect(isHistoryStateBfcacheVersionCurrent(null, 0)).toBe(false);
});
it("restores matching history snapshots and evicts the oldest entry", () => {
const cache = new HistoryStateSnapshotCache<string>({ maxEntries: 2 });
const entry1 = createHistoryStateWithNavigationMetadata(null, {
previousNextUrl: null,
traversalIndex: 1,
});
const entry2 = createHistoryStateWithNavigationMetadata(null, {
previousNextUrl: null,
traversalIndex: 2,
});
const entry3 = createHistoryStateWithNavigationMetadata(null, {
previousNextUrl: null,
traversalIndex: 3,
});
cache.remember({ bfcacheVersion: 7, historyIndex: 1, state: "one" });
cache.remember({ bfcacheVersion: 7, historyIndex: 2, state: "two" });
cache.remember({ bfcacheVersion: 7, historyIndex: 3, state: "three" });
expect(
cache.resolveRestore({
currentBfcacheVersion: 7,
guarded: false,
historyState: entry1,
}),
).toEqual({
kind: "skip",
reason: "missing-snapshot",
targetHistoryIndex: 1,
});
expect(
cache.resolveRestore({
currentBfcacheVersion: 7,
guarded: false,
historyState: entry2,
}),
).toEqual({
kind: "restore",
state: "two",
targetHistoryIndex: 2,
});
expect(
cache.resolveRestore({
currentBfcacheVersion: 7,
guarded: false,
historyState: entry3,
}),
).toEqual({
kind: "restore",
state: "three",
targetHistoryIndex: 3,
});
});
it("suppresses history snapshot restore while cache invalidation is guarded", () => {
const cache = new HistoryStateSnapshotCache<string>({ maxEntries: 2 });
const entry = createHistoryStateWithNavigationMetadata(null, {
previousNextUrl: null,
traversalIndex: 2,
});
cache.remember({ bfcacheVersion: 7, historyIndex: 2, state: "two" });
expect(
cache.resolveRestore({
currentBfcacheVersion: 7,
guarded: true,
historyState: entry,
}),
).toEqual({
kind: "skip",
reason: "guarded",
targetHistoryIndex: 2,
});
expect(
cache.resolveRestore({
currentBfcacheVersion: 7,
guarded: false,
historyState: entry,
}),
).toEqual({
kind: "restore",
state: "two",
targetHistoryIndex: 2,
});
});
it("deletes stale history snapshots when the bfcache epoch changes", () => {
const cache = new HistoryStateSnapshotCache<string>({ maxEntries: 2 });
const entry = createHistoryStateWithNavigationMetadata(null, {
previousNextUrl: null,
traversalIndex: 2,
});
cache.remember({ bfcacheVersion: 7, historyIndex: 2, state: "two" });
expect(
cache.resolveRestore({
currentBfcacheVersion: 8,
guarded: false,
historyState: entry,
}),
).toEqual({
kind: "skip",
reason: "stale-bfcache-version",
targetHistoryIndex: 2,
});
expect(
cache.resolveRestore({
currentBfcacheVersion: 7,
guarded: false,
historyState: entry,
}),
).toEqual({
kind: "skip",
reason: "missing-snapshot",
targetHistoryIndex: 2,
});
});
it("initializes restorable client state with a fresh document bfcache epoch", () => {
const initialState = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: { [pageX1Id]: "_b_9_" },
bfcacheVersion: 3,
previousNextUrl: null,
});
const controller = new RestorableClientStateController<string>({
initialHistoryState: initialState,
maxHistoryStateSnapshots: 2,
});
expect(controller.currentBfcacheVersion).toBe(4);
expect(controller.readCurrentBfcacheVersionHistoryIds(initialState)).toBeNull();
const currentState = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: { [pageX1Id]: "_b_10_" },
bfcacheVersion: controller.currentBfcacheVersion,
previousNextUrl: null,
});
expect(controller.readCurrentBfcacheVersionHistoryIds(currentState)).toEqual({
[pageX1Id]: "_b_10_",
});
});
it("keeps bfcache ids and history snapshots behind the same restorable client state guard", () => {
const controller = new RestorableClientStateController<string>({
initialHistoryState: null,
maxHistoryStateSnapshots: 2,
});
const entry = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: { [pageX1Id]: "_b_9_" },
bfcacheVersion: controller.currentBfcacheVersion,
previousNextUrl: null,
traversalIndex: 2,
});
controller.rememberHistoryStateSnapshot({ historyIndex: 2, state: "two" });
const release = controller.beginCacheInvalidationGuard();
expect(controller.readCurrentBfcacheVersionHistoryIds(entry)).toBeNull();
expect(controller.resolveHistoryStateSnapshotRestore(entry)).toEqual({
kind: "skip",
reason: "guarded",
targetHistoryIndex: 2,
});
release();
expect(controller.readCurrentBfcacheVersionHistoryIds(entry)).toEqual({
[pageX1Id]: "_b_9_",
});
expect(controller.resolveHistoryStateSnapshotRestore(entry)).toEqual({
kind: "restore",
state: "two",
targetHistoryIndex: 2,
});
});
it("keeps restorable client state guarded until nested guards are released", () => {
const controller = new RestorableClientStateController<string>({
initialHistoryState: null,
maxHistoryStateSnapshots: 2,
});
const entry = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: { [pageX1Id]: "_b_9_" },
bfcacheVersion: controller.currentBfcacheVersion,
previousNextUrl: null,
traversalIndex: 2,
});
controller.rememberHistoryStateSnapshot({ historyIndex: 2, state: "two" });
const releaseOuter = controller.beginCacheInvalidationGuard();
const releaseInner = controller.beginCacheInvalidationGuard();
releaseOuter();
releaseOuter();
expect(controller.readCurrentBfcacheVersionHistoryIds(entry)).toBeNull();
expect(controller.resolveHistoryStateSnapshotRestore(entry)).toEqual({
kind: "skip",
reason: "guarded",
targetHistoryIndex: 2,
});
releaseInner();
releaseInner();
expect(controller.readCurrentBfcacheVersionHistoryIds(entry)).toEqual({
[pageX1Id]: "_b_9_",
});
expect(controller.resolveHistoryStateSnapshotRestore(entry)).toEqual({
kind: "restore",
state: "two",
targetHistoryIndex: 2,
});
});
it("invalidates bfcache ids and history snapshots through one restorable client state epoch", () => {
const controller = new RestorableClientStateController<string>({
initialHistoryState: null,
maxHistoryStateSnapshots: 2,
});
const entry = createHistoryStateWithNavigationMetadata(null, {
bfcacheIds: { [pageX1Id]: "_b_9_" },
bfcacheVersion: controller.currentBfcacheVersion,
previousNextUrl: null,
traversalIndex: 2,
});
controller.rememberHistoryStateSnapshot({ historyIndex: 2, state: "two" });
controller.invalidateClientState();
expect(controller.readCurrentBfcacheVersionHistoryIds(entry)).toBeNull();
expect(controller.resolveHistoryStateSnapshotRestore(entry)).toEqual({
kind: "skip",
reason: "missing-snapshot",
targetHistoryIndex: 2,
});
});
it("uses restored history bfcache ids for traversal commits", async () => {
const currentState = createState({
bfcacheIds: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[pageX2Id]: "_b_8_",
},
elements: createBfcacheElements(pageX2Id),
layoutIds: [rootLayoutId, groupLayoutId],
routeId: "route:/x/2",
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(createBfcacheElements(pageX1Id)),
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/x/1", {}),
operationLane: "traverse",
renderId: 1,
restoredBfcacheIds: {
[rootLayoutId]: "0",
[groupLayoutId]: "_b_4_",
[pageX1Id]: "_b_5_",
},
type: "traverse",
});
expect(pending.action.bfcacheIds[pageX1Id]).toBe("_b_5_");
});
it("mints redirected traverse target ids after restored ids are cleared", async () => {
const currentState = createState({
bfcacheIds: {
[rootLayoutId]: "0",
[groupLayoutId]: "0",
[pageX1Id]: "_b_4_",
},
elements: createBfcacheElements(pageX1Id),
layoutIds: [rootLayoutId, groupLayoutId],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/x/1", {}),
routeId: "route:/x/1",
});
const pending = await createPendingNavigationCommit({
payloadOrigin: FRESH_APP_NAVIGATION_PAYLOAD_ORIGIN,
currentState,
nextElements: Promise.resolve(
createBfcacheElements(pageY1Id, {
bfcacheSegmentIdentities: {
[rootLayoutId]: `identity:${rootLayoutId}`,
[groupLayoutId]: "identity:group:y",
[pageY1Id]: "identity:page:y",
},
}),
),
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/y/1", {}),
operationLane: "navigation",
renderId: 1,
// navigateRsc clears the source entry's restored ids before committing a
// redirected traverse target, otherwise restored ids would win over fresh
// ids for changed dynamic segments.
restoredBfcacheIds: null,
type: "traverse",
});
expect(pending.action.bfcacheIds[rootLayoutId]).toBe("0");
expect(pending.action.bfcacheIds[groupLayoutId]).toMatch(/^_b_\d+_$/);
expect(pending.action.bfcacheIds[groupLayoutId]).not.toBe("0");
});
it("keeps future minted bfcache ids ahead of restored history state ids", () => {
const next = createNextBfcacheIdMap({
current: createInitialBfcacheIdMap(createBfcacheElements(pageX1Id)),
currentElements: createBfcacheElements(pageX1Id),
elements: createBfcacheElements(pageX2Id),
restored: {
[pageX1Id]: "_b_900000_",
},
});
const freshIdNumber = Number(/^_b_(\d+)_$/.exec(next[pageX2Id] ?? "")?.[1]);
expect(freshIdNumber).toBeGreaterThan(900000);
});
});
describe("createPopstateRestoreHandler", () => {
it("guards synchronous popstate scroll retry to the active navigation", () => {
const scrollState = { __vinext_scrollY: 10 };
let activeNavigationId = 3;
let consumedNavigationId: number | null = null;
let shouldContinue: (() => boolean) | undefined;
restoreSynchronousPopstateScrollPosition(
{
getActiveNavigationId: () => activeNavigationId,
isCurrentNavigation: (navId) => navId === activeNavigationId,
markScrollRestoreConsumed: (navId) => {
consumedNavigationId = navId;
},
restorePopstateScrollPosition: (state, options) => {
expect(state).toBe(scrollState);
shouldContinue = options?.shouldContinue;
},
},
scrollState,
);
expect(consumedNavigationId).toBe(3);
expect(shouldContinue?.()).toBe(true);
activeNavigationId = 4;
expect(shouldContinue?.()).toBe(false);
});
it("restores scroll only after the latest popstate navigation commits", async () => {
const restoreCalls: unknown[] = [];
const firstNavigation = createDeferred();
const secondNavigation = createDeferred();
let popstateCalls = 0;
const popstate = vi.fn(() => {
popstateCalls += 1;
if (popstateCalls === 1) {
return firstNavigation.promise;
}
return secondNavigation.promise;
});
let activeNavigationId = 0;
stubWindow("https://example.com/feed");
window.__VINEXT_RSC_PENDING__ = null;
const handler = createPopstateRestoreHandler({
getActiveNavigationId: () => activeNavigationId,
getNavigate: () => {
activeNavigationId += 1;
return () => popstate();
},
getPendingNavigation: () => window.__VINEXT_RSC_PENDING__,
isCurrentNavigation: (navId) => navId === activeNavigationId,
notifyAppRouterTransitionStart: () => {},
restorePopstateScrollPosition: (scrollState) => {
restoreCalls.push(scrollState);
},
setPendingNavigation: (pendingNavigation) => {
window.__VINEXT_RSC_PENDING__ = pendingNavigation;
},
shouldSkipScrollRestore: () => false,
});
handler({ state: { __vinext_scrollY: 10 } } as PopStateEvent);
handler({ state: { __vinext_scrollY: 20 } } as PopStateEvent);
expect(window.__VINEXT_RSC_PENDING__).toBe(secondNavigation.promise);
secondNavigation.resolve();
await secondNavigation.promise;
await Promise.resolve();
expect(restoreCalls).toEqual([{ __vinext_scrollY: 20 }]);
expect(window.__VINEXT_RSC_PENDING__).toBeNull();
firstNavigation.resolve();
await firstNavigation.promise;
await Promise.resolve();
expect(restoreCalls).toEqual([{ __vinext_scrollY: 20 }]);
expect(window.__VINEXT_RSC_PENDING__).toBeNull();
});
it("clears __VINEXT_RSC_PENDING__ when a stale popstate navigation settles", async () => {
const restoreCalls: unknown[] = [];
const navigation = createDeferred();
let activeNavigationId = 1;
stubWindow("https://example.com/feed");
window.__VINEXT_RSC_PENDING__ = null;
const handler = createPopstateRestoreHandler({
getActiveNavigationId: () => activeNavigationId,
getNavigate: () => {
activeNavigationId = 1;
return () => navigation.promise;
},
getPendingNavigation: () => window.__VINEXT_RSC_PENDING__,
isCurrentNavigation: (navId) => navId === activeNavigationId,
notifyAppRouterTransitionStart: () => {},
restorePopstateScrollPosition: (scrollState) => {
restoreCalls.push(scrollState);
},
setPendingNavigation: (pendingNavigation) => {
window.__VINEXT_RSC_PENDING__ = pendingNavigation;
},
shouldSkipScrollRestore: () => false,
});
handler({ state: { __vinext_scrollY: 10 } } as PopStateEvent);
expect(window.__VINEXT_RSC_PENDING__).toBe(navigation.promise);
activeNavigationId = 2;
navigation.resolve();
await navigation.promise;
await Promise.resolve();
expect(restoreCalls).toEqual([]);
expect(window.__VINEXT_RSC_PENDING__).toBeNull();
});
it("does not reapply delayed scroll after synchronous popstate restore consumed it", async () => {
const restoreCalls: unknown[] = [];
const navigation = createDeferred();
let activeNavigationId = 0;
let synchronouslyRestoredNavigationId: number | null = null;
stubWindow("https://example.com/feed");
window.__VINEXT_RSC_PENDING__ = null;
const handler = createPopstateRestoreHandler({
getActiveNavigationId: () => activeNavigationId,
getNavigate: () => {
activeNavigationId += 1;
return () => navigation.promise;
},
getPendingNavigation: () => window.__VINEXT_RSC_PENDING__,
isCurrentNavigation: (navId) => navId === activeNavigationId,
notifyAppRouterTransitionStart: () => {},
restorePopstateScrollPosition: (scrollState) => {
restoreCalls.push(scrollState);
},
setPendingNavigation: (pendingNavigation) => {
window.__VINEXT_RSC_PENDING__ = pendingNavigation;
},
shouldSkipScrollRestore: (navId) => synchronouslyRestoredNavigationId === navId,
});
handler({ state: { __vinext_scrollY: 10 } } as PopStateEvent);
synchronouslyRestoredNavigationId = activeNavigationId;
restoreCalls.push({ __vinext_scrollY: 10, source: "sync" });
await Promise.resolve();
navigation.resolve();
await navigation.promise;
await Promise.resolve();
expect(restoreCalls).toEqual([{ __vinext_scrollY: 10, source: "sync" }]);
expect(window.__VINEXT_RSC_PENDING__).toBeNull();
});
});
describe("app browser RSC redirect lifecycle", () => {
it("blocks dangerous streamed redirects before browser navigation", async () => {
const cancel = vi.fn();
const response = new Response(new ReadableStream({ cancel }));
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
expect(
blockDangerousStreamedRscRedirect(
response,
"javascript:window.location.assign('/nextjs-compat/javascript-urls/boom')",
),
).toBe(true);
await vi.waitFor(() => expect(cancel).toHaveBeenCalledOnce());
expect(consoleError).toHaveBeenCalledWith(
"Next.js has blocked a javascript: URL as a security precaution.",
);
});
it("allows safe streamed redirects to reach the navigation planner", () => {
const response = new Response("rsc payload");
expect(blockDangerousStreamedRscRedirect(response, "/dashboard")).toBe(false);
expect(response.bodyUsed).toBe(false);
});
it("keeps RSC redirect hops in the initiating lifecycle and preserves push history intent", () => {
const decision = resolveRscRedirectLifecycleHop({
currentHref: "https://example.com/start",
historyUpdateMode: "push",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: "/feed",
responseUrl: "https://example.com/target.rsc?tab=1&_rsc=abc",
});
expect(decision).toEqual({
href: "/target?tab=1",
historyUpdateMode: "push",
kind: "follow",
previousNextUrl: "/feed",
redirectDepth: 1,
});
});
it("treats same-path search changes as RSC redirects", () => {
const decision = resolveRscRedirectLifecycleHop({
currentHref: "https://example.com/items?sort=old",
historyUpdateMode: "replace",
origin: "https://example.com",
redirectDepth: 2,
requestPreviousNextUrl: null,
responseUrl: "https://example.com/items.rsc?sort=new&_rsc=abc",
});
expect(decision).toMatchObject({
href: "/items?sort=new",
historyUpdateMode: "replace",
kind: "follow",
redirectDepth: 3,
});
});
it("allows callers to model terminal traverse/refresh redirects as replace commits", () => {
const decision = resolveRscRedirectLifecycleHop({
currentHref: "https://example.com/old",
historyUpdateMode: "replace",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
responseUrl: "https://example.com/new.rsc?_rsc=abc",
});
expect(decision).toMatchObject({
href: "/new",
historyUpdateMode: "replace",
kind: "follow",
});
});
it("turns external RSC redirects into terminal hard navigations", () => {
const decision = resolveRscRedirectLifecycleHop({
currentHref: "https://example.com/account",
historyUpdateMode: "push",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
responseUrl: "https://idp.example/login",
});
expect(decision).toEqual({
href: "https://idp.example/login",
kind: "terminal-hard-navigation",
reason: "externalRedirect",
redirectDepth: 0,
});
});
it("turns an over-budget redirect chain into a terminal hard navigation", () => {
const decision = resolveRscRedirectLifecycleHop({
currentHref: "https://example.com/a",
historyUpdateMode: "push",
maxRedirectDepth: 2,
origin: "https://example.com",
redirectDepth: 2,
requestPreviousNextUrl: null,
responseUrl: "https://example.com/b.rsc?_rsc=abc",
});
expect(decision).toEqual({
href: "/b",
kind: "terminal-hard-navigation",
reason: "maxRedirectsExceeded",
redirectDepth: 2,
});
});
it("preserves streamed redirect target hashes", () => {
const decision = resolveStreamedRscRedirectLifecycleHop({
currentHref: "/old#old",
historyUpdateMode: "replace",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
streamedRedirectTarget: "/new#new",
});
expect(decision).toEqual({
href: "/new#new",
historyUpdateMode: "replace",
kind: "follow",
previousNextUrl: null,
redirectDepth: 1,
});
});
it("lets explicit push redirects upgrade an initiating replace navigation", () => {
const decision = navigationPlanner.classifyRscFetchResult({
clientCompatibilityId: null,
compatibilityIdHeader: null,
currentHref: "/old",
effectiveHistoryUpdateMode: "replace",
hasBody: true,
isRscContentType: true,
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
responseOk: true,
responseUrl: null,
source: "live",
streamedRedirectTarget: "/new",
streamedRedirectType: "push",
});
expect(decision).toMatchObject({
kind: "followRedirect",
redirect: {
href: "/new",
historyUpdateMode: "push",
},
});
});
it("preserves an initiating push across a replace-style server redirect", () => {
const decision = navigationPlanner.classifyRscFetchResult({
clientCompatibilityId: null,
compatibilityIdHeader: null,
currentHref: "/source",
effectiveHistoryUpdateMode: "push",
hasBody: true,
isRscContentType: true,
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
responseOk: true,
responseUrl: null,
source: "live",
streamedRedirectTarget: "/destination",
streamedRedirectType: "replace",
});
expect(decision).toMatchObject({
kind: "followRedirect",
redirect: {
href: "/destination",
historyUpdateMode: "push",
},
});
});
it("treats streamed hash-only same-path changes as redirects", () => {
const decision = resolveStreamedRscRedirectLifecycleHop({
currentHref: "/same#old",
historyUpdateMode: "push",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: "/feed",
streamedRedirectTarget: "/same#new",
});
expect(decision).toEqual({
href: "/same#new",
historyUpdateMode: "push",
kind: "follow",
previousNextUrl: "/feed",
redirectDepth: 1,
});
});
it("preserves streamed redirect visible query params and hash", () => {
const decision = resolveStreamedRscRedirectLifecycleHop({
currentHref: "/source",
historyUpdateMode: "replace",
origin: "https://example.com",
redirectDepth: 1,
requestPreviousNextUrl: null,
streamedRedirectTarget: "/target.rsc?visible=1&_rsc=abc#details",
});
expect(decision).toEqual({
href: "/target.rsc?visible=1&_rsc=abc#details",
historyUpdateMode: "replace",
kind: "follow",
previousNextUrl: null,
redirectDepth: 2,
});
});
it("turns same-target streamed redirects into no-redirect decisions", () => {
const decision = resolveStreamedRscRedirectLifecycleHop({
currentHref: "/same?tab=1#section",
historyUpdateMode: "replace",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
streamedRedirectTarget: "/same?tab=1#section",
});
expect(decision).toEqual({ href: "/same?tab=1#section", kind: "no-redirect" });
});
it("turns external streamed redirects into terminal hard navigations", () => {
const decision = resolveStreamedRscRedirectLifecycleHop({
currentHref: "/account",
historyUpdateMode: "replace",
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
streamedRedirectTarget: "https://idp.example/login#step",
});
expect(decision).toEqual({
href: "https://idp.example/login#step",
kind: "terminal-hard-navigation",
reason: "externalRedirect",
redirectDepth: 0,
});
});
it("maps streamed redirect history mode onto external hard navigation mode", () => {
const replaceDecision = navigationPlanner.classifyRscFetchResult({
clientCompatibilityId: null,
compatibilityIdHeader: null,
currentHref: "/account",
effectiveHistoryUpdateMode: "push",
hasBody: true,
isRscContentType: true,
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
responseOk: true,
responseUrl: null,
source: "live",
streamedRedirectTarget: "https://idp.example/login",
streamedRedirectType: "replace",
});
const pushDecision = navigationPlanner.classifyRscFetchResult({
clientCompatibilityId: null,
compatibilityIdHeader: null,
currentHref: "/account",
effectiveHistoryUpdateMode: "replace",
hasBody: true,
isRscContentType: true,
origin: "https://example.com",
redirectDepth: 0,
requestPreviousNextUrl: null,
responseOk: true,
responseUrl: null,
source: "live",
streamedRedirectTarget: "https://idp.example/login",
streamedRedirectType: "push",
});
expect(replaceDecision).toMatchObject({
hardNavigationMode: "assign",
kind: "hardNavigate",
});
expect(pushDecision).toMatchObject({
hardNavigationMode: "assign",
kind: "hardNavigate",
});
});
it("turns over-budget streamed redirects into terminal hard navigations", () => {
const decision = resolveStreamedRscRedirectLifecycleHop({
currentHref: "/a",
historyUpdateMode: "replace",
maxRedirectDepth: 2,
origin: "https://example.com",
redirectDepth: 2,
requestPreviousNextUrl: null,
streamedRedirectTarget: "/b#target",
});
expect(decision).toEqual({
href: "/b#target",
kind: "terminal-hard-navigation",
reason: "maxRedirectsExceeded",
redirectDepth: 2,
});
});
});
describe("devOnCaughtError (hydrateRoot dev handler)", () => {
it("routes the framework dev recovery boundary through the uncaught handler", () => {
const onCaughtError = vi.fn();
const onImplicitRootError = vi.fn();
const handler = createDevOnCaughtError(onCaughtError, onImplicitRootError);
const error = new Error("navigation render failed");
const errorInfo = {
componentStack: "\n at Lazy",
errorBoundary: { props: { isImplicitRootErrorBoundary: true } },
};
handler(error, errorInfo);
expect(onImplicitRootError).toHaveBeenCalledWith(error, errorInfo);
expect(onCaughtError).not.toHaveBeenCalled();
});
it("keeps explicit dev error boundaries on the caught-error path", () => {
const onCaughtError = vi.fn();
const onImplicitRootError = vi.fn();
const handler = createDevOnCaughtError(onCaughtError, onImplicitRootError);
const error = new Error("route error");
const errorInfo = {
componentStack: "\n at Page",
errorBoundary: { props: { isImplicitRootErrorBoundary: false } },
};
handler(error, errorInfo);
expect(onCaughtError).toHaveBeenCalledWith(error, errorInfo);
expect(onImplicitRootError).not.toHaveBeenCalled();
});
it("ignores redirect sentinels handled by RedirectBoundary", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
devOnCaughtError(
Object.assign(new Error("NEXT_REDIRECT:/?auth=required"), {
digest: "NEXT_REDIRECT;;%2F%3Fauth%3Drequired",
}),
{ componentStack: "\n at ProtectedPage" },
);
expect(consoleSpy).not.toHaveBeenCalled();
} finally {
consoleSpy.mockRestore();
}
});
it("logs caught errors to console.error", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const err = new Error("Maximum update depth exceeded");
devOnCaughtError(err, { componentStack: "\n at List\n at Apps" });
expect(consoleSpy).toHaveBeenCalled();
const loggedErrors = consoleSpy.mock.calls.map((args) => args[0]);
expect(loggedErrors).toContain(err);
} finally {
consoleSpy.mockRestore();
}
});
it("includes the React component stack in the log when provided", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
devOnCaughtError(new Error("boom"), {
componentStack: "\n at List (apps/list.tsx:202)",
});
expect(consoleSpy).toHaveBeenCalledTimes(2);
expect(String(consoleSpy.mock.calls[1][0])).toContain("apps/list.tsx:202");
} finally {
consoleSpy.mockRestore();
}
});
it("does not re-dispatch a window 'error' event (would trigger Vite overlay)", () => {
// This test runs in a Node environment where `window` is undefined, so the
// listener registration is skipped and windowErrorCount stays 0 trivially.
// The test still documents the contract: devOnCaughtError must not dispatch
// window error events (which would re-trigger the Vite overlay). If a DOM
// environment is ever added to this project, this will become a live check.
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
let windowErrorCount = 0;
const onError = (): void => {
windowErrorCount += 1;
};
if (typeof window !== "undefined") {
window.addEventListener("error", onError);
}
try {
devOnCaughtError(new Error("caught by user error.tsx"), {});
expect(windowErrorCount).toBe(0);
} finally {
if (typeof window !== "undefined") {
window.removeEventListener("error", onError);
}
consoleSpy.mockRestore();
}
});
it("is not a no-op (regression guard against `() => {}`)", () => {
// Explicit regression guard: the original implementation was `() => {}`,
// which silently swallowed all caught errors. This test ensures the handler
// always calls console.error at least once.
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
devOnCaughtError(new Error("regression"), {});
expect(consoleSpy.mock.calls.length).toBeGreaterThan(0);
} finally {
consoleSpy.mockRestore();
}
});
});
describe("devOnUncaughtError (hydrateRoot dev handler)", () => {
it("ignores redirect sentinels handled by global redirect recovery", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
devOnUncaughtError(
Object.assign(new Error("NEXT_REDIRECT:/?auth=required"), {
digest: "NEXT_REDIRECT;;%2F%3Fauth%3Drequired",
}),
{ componentStack: "\n at ProtectedPage" },
);
expect(consoleSpy).not.toHaveBeenCalled();
} finally {
consoleSpy.mockRestore();
}
});
});
describe("dev overlay Shadow DOM mount", () => {
class FakeShadowRoot {
children: FakeElement[] = [];
appendChild<T extends FakeElement>(node: T): T {
this.children.push(node);
return node;
}
getElementById(id: string): FakeElement | null {
return this.children.find((child) => child.id === id) ?? null;
}
}
class FakeElement {
children: FakeElement[] = [];
id = "";
shadowRoot: FakeShadowRoot | null = null;
style: Record<string, string> = {};
attributes = new Map<string, string>();
constructor(
private readonly owner: FakeDocument | null,
readonly tagName: string,
) {}
appendChild<T extends FakeElement>(node: T): T {
this.children.push(node);
this.owner?.registerTree(node);
return node;
}
attachShadow(init: { mode: "open" | "closed" }): FakeShadowRoot {
expect(init.mode).toBe("open");
this.shadowRoot = new FakeShadowRoot();
return this.shadowRoot;
}
setAttribute(name: string, value: string): void {
this.attributes.set(name, value);
}
}
class FakeDocument {
private readonly elementsById = new Map<string, FakeElement>();
readonly body = new FakeElement(this, "body");
readonly documentElement = new FakeElement(this, "html");
createElement(tagName: string): FakeElement {
return new FakeElement(this, tagName);
}
getElementById(id: string): FakeElement | null {
return this.elementsById.get(id) ?? null;
}
registerTree(node: FakeElement): void {
if (node.id) this.elementsById.set(node.id, node);
for (const child of node.children) this.registerTree(child);
}
}
it("mounts the React root inside an open shadow root", () => {
const fakeDocument = new FakeDocument();
const mount = createDevErrorOverlayMountNode(fakeDocument as unknown as Document);
const host = fakeDocument.getElementById(DEV_ERROR_OVERLAY_HOST_ID);
expect(host).not.toBeNull();
expect(host?.shadowRoot).not.toBeNull();
expect(mount.id).toBe(DEV_ERROR_OVERLAY_MOUNT_ID);
expect(host?.shadowRoot?.getElementById(DEV_ERROR_OVERLAY_MOUNT_ID)).toBe(mount);
expect(host?.children).toHaveLength(0);
});
it("keeps the shadow host out of normal page layout", () => {
const fakeDocument = new FakeDocument();
createDevErrorOverlayMountNode(fakeDocument as unknown as Document);
const host = fakeDocument.getElementById(DEV_ERROR_OVERLAY_HOST_ID);
expect(host?.style.position).toBe("absolute");
expect(host?.style.width).toBe("0");
expect(host?.style.height).toBe("0");
expect(host?.style.overflow).toBe("visible");
expect(host?.attributes.get("data-vinext-dev-error-overlay")).toBe("");
});
it("reuses the existing host and shadow mount across installs", () => {
const fakeDocument = new FakeDocument();
const firstMount = createDevErrorOverlayMountNode(fakeDocument as unknown as Document);
const secondMount = createDevErrorOverlayMountNode(fakeDocument as unknown as Document);
expect(secondMount).toBe(firstMount);
expect(fakeDocument.body.children).toHaveLength(1);
expect(fakeDocument.body.children[0]?.shadowRoot?.children).toHaveLength(1);
});
});
describe("dev overlay open-in-editor helpers", () => {
it("formats overlay display files relative to the project root", () => {
expect(
formatOverlayDisplayFile(
"file:///Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx",
"/Users/hyoban/f/vinext/apps/web",
),
).toBe("app/_components/site-footer.tsx");
expect(
formatOverlayDisplayFile(
"/Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx",
"/Users/hyoban/f/vinext/apps/web",
),
).toBe("app/_components/site-footer.tsx");
expect(
formatOverlayDisplayFile(
"/Users/hyoban/f/vinext/packages/vinext/src/server/dev-error-overlay.tsx",
"/Users/hyoban/f/vinext/apps/web",
),
).toBe("/Users/hyoban/f/vinext/packages/vinext/src/server/dev-error-overlay.tsx");
expect(
formatOverlayDisplayFile(
"about://React/Server/file:///Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx?9",
"/Users/hyoban/f/vinext/apps/web/",
),
).toBe("app/_components/site-footer.tsx");
});
it("formats stack frames as Vite open-in-editor file payloads", () => {
expect(
formatViteOpenInEditorFile({
file: "/Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx",
line: "9",
col: "8",
}),
).toBe("/Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx:9:8");
expect(
formatViteOpenInEditorFile({
file: "/Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx",
line: "9",
}),
).toBe("/Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx:9");
expect(formatViteOpenInEditorFile({ file: "virtual:vinext-app-browser-entry" })).toBe(
"virtual:vinext-app-browser-entry",
);
expect(
formatViteOpenInEditorFile({
file: "about://React/Server/file:///Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx?9",
line: "9",
col: "8",
}),
).toBe("/Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx:9:8");
expect(formatViteOpenInEditorFile({})).toBeNull();
});
it("builds the Vite dev server open-in-editor URL", () => {
expect(
createViteOpenInEditorUrl(
"/Users/hyoban/f/vinext/apps/web/app/_components/site footer.tsx:9:8",
"http://localhost:3001/@id/__x00__virtual:vinext-app-browser-entry",
),
).toBe(
"http://localhost:3001/__open-in-editor?file=%2FUsers%2Fhyoban%2Ff%2Fvinext%2Fapps%2Fweb%2Fapp%2F_components%2Fsite%20footer.tsx%3A9%3A8",
);
});
it("formats copied error info with visible stack frames and code frames", () => {
expect(
formatErrorInfoForClipboard(
{
source: "server",
message: "vinext is not ready yet. Stay tuned!",
projectRoot: "/Users/hyoban/f/vinext/apps/web",
codeFrame: {
file: "file:///Users/hyoban/f/vinext/apps/web/app/_components/site-footer.tsx",
line: 6,
column: 9,
methodName: "SiteFooter",
lines: [
{ line: 4, text: "const unused = 1;", isErrorLine: false },
{ line: 5, text: "export function SiteFooter() {", isErrorLine: false },
{
line: 6,
text: ' throw new Error("vinext is not ready yet. Stay tuned!");',
isErrorLine: true,
},
{ line: 7, text: " return (", isErrorLine: false },
],
},
},
[
{
fn: "SiteFooter",
displayFile: "app/_components/site-footer.tsx",
line: "6",
col: "9",
ignored: false,
},
{
fn: "renderFunctionComponent",
displayFile: "node_modules/.vite/deps_rsc/react-server-dom-webpack_server__edge.js",
line: "956",
col: "71",
ignored: true,
},
],
),
).toBe(
[
"## Error Type",
"",
"Server Error",
"",
"## Error Message",
"",
"vinext is not ready yet. Stay tuned!",
"",
"## Stack",
"",
" at SiteFooter (app/_components/site-footer.tsx:6:9)",
"",
"## Code Frame",
"",
"app/_components/site-footer.tsx:6:9 @ SiteFooter",
" 4 | const unused = 1;",
" 5 | export function SiteFooter() {",
'> 6 | throw new Error("vinext is not ready yet. Stay tuned!");',
" | ^",
" 7 | return (",
].join("\n"),
);
});
it("formats copied build errors without Vite internal stack frames", () => {
expect(
formatErrorInfoForClipboard(
{
source: "vite",
message:
"[plugin:vite:oxc] Transform failed with 1 error:\n\n[PARSE_ERROR] Error: Unterminated string",
codeFrame: {
file: "app/_components/site-footer.tsx",
line: 7,
column: 90,
lines: [
{
line: 7,
text: '<div className="broken>',
isErrorLine: true,
},
],
},
},
[
{
fn: "transformWithOxc",
displayFile: "node_modules/@voidzero-dev/vite-plus-core/dist/vite/node.js",
line: "5987",
col: "19",
ignored: false,
},
],
),
).toBe(
[
"## Error Type",
"",
"Build Error",
"",
"## Error Message",
"",
"[plugin:vite:oxc] Transform failed with 1 error:",
"",
"[PARSE_ERROR] Error: Unterminated string",
].join("\n"),
);
});
});
describe("dev overlay Vite HMR errors", () => {
it("normalizes Vite transform errors into build-error overlay data", () => {
const normalized = normalizeViteHmrError({
err: {
message:
"Transform failed with 1 error:\napp/_components/site-footer.tsx:7:90: ERROR: Unterminated string",
plugin: "vite:esbuild",
frame: [
" 5 | return (",
' 6 | <footer className="mt-auto border-t">',
'> 7 | <div className="mx-auto flex w-full max-w-6xl items-center justify-center px-6 py-6">',
" | ^",
" 8 | <Text>vinext</Text>",
].join("\n"),
},
});
expect(normalized.message).toContain("[plugin:vite:esbuild] Transform failed with 1 error");
expect(normalized.message).toContain(
'> 7 | <div className="mx-auto flex w-full max-w-6xl items-center justify-center px-6 py-6">',
);
});
it("normalizes OXC build error frames from Vite+", () => {
const message = [
"Transform failed with 1 error:",
"",
"[PARSE_ERROR] Error: Unterminated string",
" ╭─[ app/dev-overlay-hmr-toggle/server-hmr-toggle.tsx:2:55 ]",
" │",
' 2 │ ╭─▶ return <p data-testid="server-hmr-toggle" className="broken>server hmr clean</p>;',
" 3 │ ├─▶ }",
" │ │ ",
" │ ╰─────── ",
"───╯",
].join("\n");
const normalized = normalizeViteHmrError({
err: {
message,
},
});
expect(normalized.message).toBe(message);
});
});
describe("dev overlay React Refresh recovery", () => {
it("dispatches recovery before React Refresh retries failed roots", async () => {
const callbacks: Array<() => void | Promise<void>> = [];
const dispatchEvent = vi.fn();
vi.stubGlobal("window", {
__registerBeforePerformReactRefresh(cb: () => void | Promise<void>) {
callbacks.push(cb);
},
dispatchEvent,
setTimeout: vi.fn(),
});
installReactRefreshErrorRecovery();
expect(callbacks).toHaveLength(1);
await callbacks[0]?.();
expect(dispatchEvent).toHaveBeenCalledTimes(1);
const event = dispatchEvent.mock.calls[0]?.[0];
expect(event).toBeInstanceOf(Event);
expect((event as Event).type).toBe(VINEXT_DEV_ERROR_RECOVERY_EVENT);
});
it("does not register duplicate React Refresh recovery callbacks", () => {
const callbacks: Array<() => void | Promise<void>> = [];
vi.stubGlobal("window", {
__registerBeforePerformReactRefresh(cb: () => void | Promise<void>) {
callbacks.push(cb);
},
dispatchEvent: vi.fn(),
setTimeout: vi.fn(),
});
installReactRefreshErrorRecovery();
installReactRefreshErrorRecovery();
expect(callbacks).toHaveLength(1);
});
it("retries until the React Refresh runtime hook is available", async () => {
const callbacks: Array<() => void | Promise<void>> = [];
const timeoutCallbacks: Array<() => void> = [];
const dispatchEvent = vi.fn();
const setTimeout = vi.fn((cb: () => void, _delay?: number) => {
timeoutCallbacks.push(cb);
return timeoutCallbacks.length;
});
const refreshWindow: {
__registerBeforePerformReactRefresh?: (cb: () => void | Promise<void>) => void;
dispatchEvent: typeof dispatchEvent;
setTimeout: typeof setTimeout;
} = {
dispatchEvent,
setTimeout,
};
vi.stubGlobal("window", refreshWindow);
installReactRefreshErrorRecovery();
expect(setTimeout).toHaveBeenCalledTimes(1);
expect(setTimeout.mock.calls[0]?.[1]).toBe(0);
await Promise.resolve();
expect(setTimeout).toHaveBeenCalledTimes(1);
timeoutCallbacks.shift()?.();
expect(setTimeout).toHaveBeenCalledTimes(2);
expect(setTimeout.mock.calls[1]?.[1]).toBe(16);
refreshWindow.__registerBeforePerformReactRefresh = (cb) => {
callbacks.push(cb);
};
timeoutCallbacks.shift()?.();
expect(callbacks).toHaveLength(1);
await callbacks[0]?.();
expect(dispatchEvent).toHaveBeenCalledTimes(1);
});
});
describe("dev overlay store", () => {
it("does not notify subscribers when an empty overlay is dismissed", () => {
const listener = vi.fn();
const unsubscribe = subscribeOverlay(listener);
try {
dismissOverlay();
expect(listener).not.toHaveBeenCalled();
reportToOverlay({
source: "caught",
message: "boom",
stack: undefined,
ignoredStackFrames: undefined,
projectRoot: undefined,
codeFrame: undefined,
componentStack: undefined,
});
expect(listener).toHaveBeenCalledTimes(1);
dismissOverlay();
expect(listener).toHaveBeenCalledTimes(2);
dismissOverlay();
expect(listener).toHaveBeenCalledTimes(2);
} finally {
unsubscribe();
dismissOverlay();
}
});
});
describe("createOnUncaughtError (hydrateRoot uncaught handler)", () => {
function withFakeWindow<T>(
fn: (spies: {
assignSpy: ReturnType<typeof vi.fn>;
reportErrorSpy: ReturnType<typeof vi.fn>;
}) => T,
): T {
const assignSpy = vi.fn();
const reportErrorSpy = vi.fn();
const originalWindow = (globalThis as { window?: unknown }).window;
const originalReportError = Object.getOwnPropertyDescriptor(globalThis, "reportError");
(globalThis as { window?: unknown }).window = {
location: { assign: assignSpy },
};
Object.defineProperty(globalThis, "reportError", {
configurable: true,
value: reportErrorSpy,
writable: true,
});
try {
return fn({ assignSpy, reportErrorSpy });
} finally {
if (originalWindow === undefined) {
delete (globalThis as { window?: unknown }).window;
} else {
(globalThis as { window?: unknown }).window = originalWindow;
}
if (originalReportError) {
Object.defineProperty(globalThis, "reportError", originalReportError);
} else {
delete (globalThis as { reportError?: unknown }).reportError;
}
}
}
it("reports the error globally without writing to console.error", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
withFakeWindow(({ reportErrorSpy }) => {
const handler = createOnUncaughtError();
const err = new Error("boom");
handler(err, { componentStack: "\n at Page (page.tsx:10)" });
expect(reportErrorSpy).toHaveBeenCalledWith(err);
expect(consoleSpy).not.toHaveBeenCalled();
});
} finally {
consoleSpy.mockRestore();
}
});
it("leaves navigation failure dispatch to the global error listener", () => {
withFakeWindow(({ assignSpy, reportErrorSpy }) => {
const handler = createOnUncaughtError();
const error = new Error("late error");
handler(error, {});
expect(reportErrorSpy).toHaveBeenCalledWith(error);
expect(assignSpy).not.toHaveBeenCalled();
});
});
});
describe("app navigation failure handling", () => {
it("hard-navigates to the latest pending URL when enabled", () => {
vi.stubEnv("__NEXT_APP_NAV_FAIL_HANDLING", "true");
const originalWindow = globalThis.window;
const assign = vi.fn();
globalThis.window = {
location: { assign, href: "https://example.com/current" },
next: { version: "vinext" },
} as unknown as Window & typeof globalThis;
try {
stageAppNavigationFailureTarget("/first");
stageAppNavigationFailureTarget("/latest");
expect(handleAppNavigationFailure(new Error("boom"))).toBe(true);
expect(assign).toHaveBeenCalledWith("https://example.com/latest");
} finally {
globalThis.window = originalWindow;
vi.unstubAllEnvs();
}
});
it("clears only the matching committed URL", () => {
vi.stubEnv("__NEXT_APP_NAV_FAIL_HANDLING", "true");
const originalWindow = globalThis.window;
globalThis.window = {
location: { href: "https://example.com/current" },
next: { version: "vinext" },
} as unknown as Window & typeof globalThis;
try {
stageAppNavigationFailureTarget("/latest");
clearAppNavigationFailureTarget("/older");
expect(window.next?.__pendingUrl?.pathname).toBe("/latest");
clearAppNavigationFailureTarget("/latest");
expect(window.next?.__pendingUrl).toBeUndefined();
} finally {
globalThis.window = originalWindow;
vi.unstubAllEnvs();
}
});
});
describe("prodOnCaughtError (hydrateRoot prod handler)", () => {
it("ignores redirect sentinels handled by RedirectBoundary", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
prodOnCaughtError(
Object.assign(new Error("NEXT_REDIRECT:/result"), {
digest: "NEXT_REDIRECT;;%2Fresult",
}),
{ componentStack: "\n at Root" },
);
expect(consoleSpy).not.toHaveBeenCalled();
} finally {
consoleSpy.mockRestore();
}
});
it("ignores notFound and HTTP fallback sentinels (notFound/forbidden/unauthorized)", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
prodOnCaughtError(Object.assign(new Error("NEXT_NOT_FOUND"), { digest: "NEXT_NOT_FOUND" }), {
componentStack: "\n at Page",
});
prodOnCaughtError(
Object.assign(new Error("NEXT_HTTP_ERROR_FALLBACK;403"), {
digest: "NEXT_HTTP_ERROR_FALLBACK;403",
}),
{ componentStack: "\n at Page" },
);
expect(consoleSpy).not.toHaveBeenCalled();
} finally {
consoleSpy.mockRestore();
}
});
it("forwards real caught errors to console.error", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const err = new Error("Maximum update depth exceeded");
prodOnCaughtError(err, { componentStack: "\n at List\n at Apps" });
const loggedErrors = consoleSpy.mock.calls.map((args) => args[0]);
expect(loggedErrors).toContain(err);
} finally {
consoleSpy.mockRestore();
}
});
it("routes implicit root-boundary errors through the uncaught handler", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const onImplicitRootError = vi.fn();
const handler = createProdOnCaughtError(onImplicitRootError);
const err = new Error("hydration mismatch");
handler(err, {
componentStack: "\n at Lazy",
errorBoundary: {
props: { isImplicitRootErrorBoundary: true },
},
});
expect(onImplicitRootError).toHaveBeenCalledWith(
err,
expect.objectContaining({ componentStack: "\n at Lazy" }),
);
expect(consoleSpy).not.toHaveBeenCalled();
} finally {
consoleSpy.mockRestore();
}
});
it("does not treat explicit segment boundaries as implicit", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const onImplicitRootError = vi.fn();
const handler = createProdOnCaughtError(onImplicitRootError);
const err = new Error("segment error");
handler(err, {
componentStack: "\n at Page",
errorBoundary: {
props: { isImplicitRootErrorBoundary: false },
},
});
expect(onImplicitRootError).not.toHaveBeenCalled();
expect(consoleSpy.mock.calls.map((args) => args[0])).toContain(err);
} finally {
consoleSpy.mockRestore();
}
});
it("includes the React component stack in the log when provided", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
prodOnCaughtError(new Error("boom"), {
componentStack: "\n at List (apps/list.tsx:202)",
});
expect(consoleSpy).toHaveBeenCalledTimes(2);
expect(String(consoleSpy.mock.calls[1][0])).toContain("apps/list.tsx:202");
} finally {
consoleSpy.mockRestore();
}
});
it("logs only the error when no component stack is provided", () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const err = new Error("no stack");
prodOnCaughtError(err, {});
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(consoleSpy.mock.calls[0][0]).toBe(err);
} finally {
consoleSpy.mockRestore();
}
});
it("does not swallow a plain error that merely mentions NEXT_REDIRECT (no digest)", () => {
// Classification is digest-based; an error whose *message* contains the
// sentinel text but has no framework digest must still be logged.
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
try {
const err = new Error("user code threw: NEXT_REDIRECT");
prodOnCaughtError(err, {});
expect(consoleSpy.mock.calls.map((args) => args[0])).toContain(err);
} finally {
consoleSpy.mockRestore();
}
});
});
describe("prodOnRecoverableError (hydrateRoot prod handler)", () => {
function withFakeReportError<T>(fn: (reportErrorSpy: ReturnType<typeof vi.fn>) => T): T {
const reportErrorSpy = vi.fn();
const originalDescriptor = Object.getOwnPropertyDescriptor(globalThis, "reportError");
Object.defineProperty(globalThis, "reportError", {
configurable: true,
value: reportErrorSpy,
writable: true,
});
try {
return fn(reportErrorSpy);
} finally {
if (originalDescriptor) {
Object.defineProperty(globalThis, "reportError", originalDescriptor);
} else {
delete (globalThis as { reportError?: unknown }).reportError;
}
}
}
it("reports recoverable hydration errors through reportError", () => {
withFakeReportError((reportErrorSpy) => {
const err = new Error("Minified React error #418");
prodOnRecoverableError(err);
expect(reportErrorSpy).toHaveBeenCalledWith(err);
});
});
it("reports the underlying cause when React provides one", () => {
withFakeReportError((reportErrorSpy) => {
const cause = new Error("server/client text mismatch");
const err = new Error("recoverable", { cause });
prodOnRecoverableError(err);
expect(reportErrorSpy).toHaveBeenCalledWith(cause);
});
});
});
describe("app browser form-state hydration", () => {
it("schedules App Router hydrateRoot inside a transition", () => {
const container = { nodeType: 1 } as Element;
const root = { render: vi.fn(), unmount: vi.fn() };
const callOrder: string[] = [];
const hydrateRoot = vi.fn(() => {
callOrder.push("hydrateRoot");
return root;
});
const startTransition = vi.fn((action: () => void) => {
callOrder.push("transition:start");
action();
callOrder.push("transition:end");
});
const result = hydrateRootInTransition({
children: "root",
container,
hydrateRoot,
options: {},
startTransition,
});
expect(result).toBe(root);
expect(startTransition).toHaveBeenCalledTimes(1);
expect(hydrateRoot).toHaveBeenCalledWith(container, "root", {});
expect(callOrder).toEqual(["transition:start", "hydrateRoot", "transition:end"]);
});
it("passes the one-shot form-state bootstrap payload to hydrateRoot options", () => {
const formState = ["action-result", "key-path", "reference-id", 1] as never;
const global = { [RSC_FORM_STATE_GLOBAL]: formState };
const onCaughtError = vi.fn();
const onRecoverableError = vi.fn();
const onUncaughtError = vi.fn();
const hydrateRoot = vi.fn();
const consumedFormState = consumeInitialFormState(global);
const hydrateOptions = createVinextHydrateRootOptions({
formState: consumedFormState,
onCaughtError,
onRecoverableError,
onUncaughtError,
});
hydrateRoot("document", "root", hydrateOptions);
expect(global).not.toHaveProperty(RSC_FORM_STATE_GLOBAL);
expect(hydrateRoot).toHaveBeenCalledWith(
"document",
"root",
expect.objectContaining({ formState }),
);
expect(hydrateOptions).toEqual({
formState,
onCaughtError,
onRecoverableError,
onUncaughtError,
});
});
it("preserves null form state as an explicit hydrateRoot option", () => {
const onUncaughtError = vi.fn();
expect(
createVinextHydrateRootOptions({
formState: consumeInitialFormState({}),
onUncaughtError,
}),
).toEqual({
formState: null,
onUncaughtError,
});
});
});
describe("mounted slot helpers", () => {
it("collects only mounted slot ids", () => {
const elements: AppElements = createResolvedElements("route:/dashboard", "/", null, {
"layout:/": React.createElement("div", null, "layout"),
"slot:children:/dashboard": React.createElement("div", null, "children"),
"slot:modal:/": React.createElement("div", null, "modal"),
"slot:sidebar:/": React.createElement("div", null, "sidebar"),
"slot:ghost:/": null,
"slot:missing:/": UNMATCHED_SLOT,
});
expect(getMountedSlotIds(elements)).toEqual(["slot:modal:/", "slot:sidebar:/"]);
});
it("serializes mounted slot ids into a stable header value", () => {
const elements: AppElements = createResolvedElements("route:/dashboard", "/", null, {
"slot:z:/": React.createElement("div", null, "z"),
"slot:a:/": React.createElement("div", null, "a"),
});
expect(getMountedSlotIdsHeader(elements)).toBe("slot:a:/ slot:z:/");
});
it("returns null when there are no mounted slots", () => {
const elements: AppElements = createResolvedElements("route:/dashboard", "/", null, {
"slot:ghost:/": null,
"slot:missing:/": UNMATCHED_SLOT,
});
expect(getMountedSlotIdsHeader(elements)).toBeNull();
});
});
describe("resolveServerActionRequestState", () => {
it("includes the public Next.js action header when previousNextUrl is null and no slots are mounted", () => {
const elements = createResolvedElements("route:/settings", "/");
const { headers } = resolveServerActionRequestState({
actionId: "action-abc",
basePath: "",
elements,
previousNextUrl: null,
});
// Ported from Next.js: test/e2e/app-dir/actions/app-action.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/actions/app-action.test.ts
expect(Array.from(headers.keys()).sort()).toEqual([
"accept",
"next-action",
"rsc",
"x-rsc-action",
]);
expect(headers.get("accept")).toBe("text/x-component");
expect(headers.get("next-action")).toBe("action-abc");
expect(headers.get("rsc")).toBe("1");
expect(headers.get("x-rsc-action")).toBe("action-abc");
});
it("derives X-Vinext-Interception-Context from previousNextUrl", () => {
const elements = createResolvedElements("route:/photos/42", "/");
const previousNextUrl = "/feed?tab=latest";
const { headers } = resolveServerActionRequestState({
actionId: "bump-likes",
basePath: "",
elements,
previousNextUrl,
});
expect(headers.get("X-Vinext-Interception-Context")).toBe(
resolveInterceptionContextFromPreviousNextUrl(previousNextUrl, ""),
);
});
it("falls back to the active interception context for server action requests", () => {
const elements = createResolvedElements("route:/dynamic-interception-revalidate/en", "/");
const { headers } = resolveServerActionRequestState({
actionId: "revalidate-photo",
basePath: "",
elements,
interceptionContext: "/dynamic-interception-revalidate/en",
previousNextUrl: null,
});
expect(headers.get("X-Vinext-Interception-Context")).toBe(
"/dynamic-interception-revalidate/en",
);
});
it("strips the base path when deriving the interception context", () => {
const elements = createResolvedElements("route:/photos/42", "/");
const previousNextUrl = "/app/feed";
const { headers } = resolveServerActionRequestState({
actionId: "bump-likes",
basePath: "/app",
elements,
previousNextUrl,
});
expect(headers.get("X-Vinext-Interception-Context")).toBe(
resolveInterceptionContextFromPreviousNextUrl(previousNextUrl, "/app"),
);
});
it("derives X-Vinext-Mounted-Slots from mounted slot keys", () => {
const elements: AppElements = createResolvedElements("route:/feed", "/", null, {
"slot:@modal:/feed": React.createElement("div", null, "modal"),
"slot:@sidebar:/feed": React.createElement("div", null, "sidebar"),
});
const { headers } = resolveServerActionRequestState({
actionId: "action-x",
basePath: "",
elements,
previousNextUrl: null,
});
expect(headers.get("X-Vinext-Mounted-Slots")).toBe(getMountedSlotIdsHeader(elements));
});
it("omits headers whose derived values are null", () => {
const elements: AppElements = createResolvedElements("route:/settings", "/", null, {
"slot:ghost:/": null,
"slot:missing:/": UNMATCHED_SLOT,
});
const { headers } = resolveServerActionRequestState({
actionId: "action-y",
basePath: "",
elements,
previousNextUrl: null,
});
expect(headers.has("X-Vinext-Interception-Context")).toBe(false);
expect(headers.has("X-Vinext-Mounted-Slots")).toBe(false);
});
});