Files
cloudflare__vinext/tests/app-browser-server-action-client.test.ts
T
James Anderson 2da8287e89 fix(app-router): honor basePath false rewrites (#2370)
* fix(app-router): honor basePath false rewrites

* fix(app-router): preserve basePath routing boundaries

* test(app-router): cover basePath handler boundaries

* fix(app-router): track basePath rewrite claims

* chore(app-router): remove stale basePath note

* fix(app-router): allow late rewrite route posts

* fix(app-router): short-circuit unclaimed basePath misses

* fix(app-router): preserve out-of-basePath 404 semantics

* fix(app-router): validate late basePath rewrites

* docs(app-router): clarify late action rewrites

* fix(app-router): resolve progressive action rewrites
2026-06-30 21:16:27 +01:00

168 lines
5.4 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { createClientNavigationRenderSnapshot } from "../packages/vinext/src/shims/navigation.js";
import { createServerActionInitiationSnapshot } from "../packages/vinext/src/server/app-browser-action-result.js";
import { invokeClientServerAction } from "../packages/vinext/src/server/app-browser-server-action-client.js";
import type { AppRouterState } from "../packages/vinext/src/server/app-browser-state.js";
import {
AppElementsWire,
normalizeAppElements,
} from "../packages/vinext/src/server/app-elements.js";
import { ACTION_REDIRECT_HEADER } from "../packages/vinext/src/server/headers.js";
import { navigationPlanner } from "../packages/vinext/src/server/navigation-planner.js";
vi.mock("@vitejs/plugin-rsc/browser", () => ({
createFromFetch: vi.fn(),
createTemporaryReferenceSet: vi.fn(() => new Map()),
encodeReply: vi.fn(async () => "encoded-action-args"),
}));
afterEach(() => {
vi.unstubAllGlobals();
});
describe("app browser server action client", () => {
it("uses action state captured before the lazy client loads", async () => {
const elements = normalizeAppElements(
AppElementsWire.createMetadataEntries({
interception: null,
interceptionContext: null,
layoutIds: [AppElementsWire.encodeLayoutId("/")],
rootLayoutTreePath: "/",
routeId: "route:/original",
slotBindings: [],
}),
);
const routerState: AppRouterState = {
activeOperation: null,
bfcacheIds: {},
elements,
interception: null,
interceptionContext: null,
layoutFlags: {},
layoutIds: [AppElementsWire.encodeLayoutId("/")],
navigationSnapshot: createClientNavigationRenderSnapshot("https://example.com/original", {}),
previousNextUrl: null,
renderId: 0,
rootLayoutTreePath: "/",
routeId: "route:/original",
slotBindings: [],
visibleCommitVersion: 0,
};
const actionInitiation = createServerActionInitiationSnapshot({
href: "https://example.com/original?tab=1",
navigationId: 42,
routerState,
});
vi.stubGlobal("window", {
location: {
href: "https://example.com/newer",
origin: "https://example.com",
},
});
const fetchMock = vi.fn().mockResolvedValue(
new Response(null, {
status: 303,
headers: {
[ACTION_REDIRECT_HEADER]: "/target",
"content-type": "text/plain",
},
}),
);
vi.stubGlobal("fetch", fetchMock);
await invokeClientServerAction("action-id", [], actionInitiation, {
basePath: "",
clearClientNavigationCaches: vi.fn(),
clientRscCompatibilityId: null,
commitSameUrlNavigatePayload: vi.fn(),
navigationPlanner,
performHardNavigation: vi.fn(),
renderRedirectPayload: vi.fn(),
syncCurrentHistoryState: vi.fn(),
syncServerActionHttpFallbackHead: vi.fn(),
});
expect(fetchMock).toHaveBeenCalledWith(
"/original?tab=1",
expect.objectContaining({ method: "POST" }),
);
});
// Ported from Next.js: test/e2e/app-dir/app-basepath/index.test.ts
// https://github.com/vercel/next.js/blob/v16.2.6/test/e2e/app-dir/app-basepath/index.test.ts
it("hard navigates for same-origin action redirects outside basePath", async () => {
const elements = normalizeAppElements(
AppElementsWire.createMetadataEntries({
interception: null,
interceptionContext: null,
layoutIds: [AppElementsWire.encodeLayoutId("/")],
rootLayoutTreePath: "/",
routeId: "route:/client",
slotBindings: [],
}),
);
const routerState: AppRouterState = {
activeOperation: null,
bfcacheIds: {},
elements,
interception: null,
interceptionContext: null,
layoutFlags: {},
layoutIds: [AppElementsWire.encodeLayoutId("/")],
navigationSnapshot: createClientNavigationRenderSnapshot(
"https://example.com/base/client",
{},
),
previousNextUrl: null,
renderId: 0,
rootLayoutTreePath: "/",
routeId: "route:/client",
slotBindings: [],
visibleCommitVersion: 0,
};
const actionInitiation = createServerActionInitiationSnapshot({
href: "https://example.com/base/client",
navigationId: 1,
routerState,
});
vi.stubGlobal("window", {
location: {
href: "https://example.com/base/client",
origin: "https://example.com",
},
});
vi.stubGlobal(
"fetch",
vi.fn().mockResolvedValue(
new Response(null, {
status: 303,
headers: {
[ACTION_REDIRECT_HEADER]: "https://example.com/outsideBasePath",
"content-type": "text/plain",
},
}),
),
);
const performHardNavigation = vi.fn();
const renderRedirectPayload = vi.fn();
await invokeClientServerAction("action-id", [], actionInitiation, {
basePath: "/base",
clearClientNavigationCaches: vi.fn(),
clientRscCompatibilityId: null,
commitSameUrlNavigatePayload: vi.fn(),
navigationPlanner,
performHardNavigation,
renderRedirectPayload,
syncCurrentHistoryState: vi.fn(),
syncServerActionHttpFallbackHead: vi.fn(),
});
expect(performHardNavigation).toHaveBeenCalledWith(
"https://example.com/outsideBasePath",
undefined,
);
expect(renderRedirectPayload).not.toHaveBeenCalled();
});
});