mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
2da8287e89
* 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
168 lines
5.4 KiB
TypeScript
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();
|
|
});
|
|
});
|