mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
a5f0bb44cb
* fix(app-router): match soft navigation identity * fix(app-router): refresh exact links behind basePath * fix(app-router): preserve navigation URL spaces * fix(app-router): preserve raw navigation identity * fix(app-router): preserve licensed history cache reuse * fix(app-router): replace visited route aliases * fix(app-router): preserve exact hash cache bypass * test(navigation): cover cache bypass runtime argument
262 lines
8.8 KiB
TypeScript
262 lines
8.8 KiB
TypeScript
import { describe, expect, it } from "vite-plus/test";
|
|
import {
|
|
NAVIGATION_TRACE_SCHEMA_VERSION,
|
|
NavigationTraceReasonCodes,
|
|
} from "../packages/vinext/src/server/navigation-trace.js";
|
|
import {
|
|
navigationPlanner,
|
|
type EarlyNavigationIntentDecision,
|
|
type EarlyNavigationIntentFacts,
|
|
} from "../packages/vinext/src/server/navigation-planner.js";
|
|
|
|
function createFacts(
|
|
overrides: Partial<EarlyNavigationIntentFacts> = {},
|
|
): EarlyNavigationIntentFacts {
|
|
return {
|
|
basePath: "",
|
|
currentUrlSpace: "browser",
|
|
currentHref: "https://example.com/docs?q=1",
|
|
mode: "push",
|
|
scroll: true,
|
|
targetHref: "https://example.com/docs?q=1#section",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function classify(
|
|
overrides: Partial<EarlyNavigationIntentFacts> = {},
|
|
): EarlyNavigationIntentDecision {
|
|
return navigationPlanner.classifyEarlyNavigationIntent(createFacts(overrides));
|
|
}
|
|
|
|
function expectSingleTraceEntry(
|
|
decision: EarlyNavigationIntentDecision,
|
|
code: string,
|
|
fields: Record<string, string | number | boolean | null>,
|
|
): void {
|
|
expect(decision.trace).toEqual({
|
|
schemaVersion: NAVIGATION_TRACE_SCHEMA_VERSION,
|
|
entries: [{ code, fields }],
|
|
});
|
|
}
|
|
|
|
describe("navigationPlanner early navigation intent classification", () => {
|
|
it("classifies a same-page hash change as a same-document scroll", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs?q=1",
|
|
targetHref: "https://example.com/docs?q=1#section",
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
kind: "sameDocumentScroll",
|
|
hash: "#section",
|
|
mode: "push",
|
|
scroll: true,
|
|
trace: decision.trace,
|
|
});
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.sameDocumentScroll, {
|
|
targetHref: "https://example.com/docs?q=1#section",
|
|
});
|
|
});
|
|
|
|
it("preserves replace mode and scroll intent on a same-document scroll", () => {
|
|
const decision = classify({
|
|
mode: "replace",
|
|
scroll: false,
|
|
currentHref: "https://example.com/docs",
|
|
targetHref: "https://example.com/docs#footer",
|
|
});
|
|
|
|
expect(decision).toMatchObject({
|
|
kind: "sameDocumentScroll",
|
|
hash: "#footer",
|
|
mode: "replace",
|
|
scroll: false,
|
|
});
|
|
});
|
|
|
|
it("resolves a relative hash target against the current href", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs?q=1",
|
|
targetHref: "#section",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "sameDocumentScroll", hash: "#section" });
|
|
});
|
|
|
|
it("classifies a same-path search change as a cache-bypassing flight navigation", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs?q=1",
|
|
targetHref: "https://example.com/docs?q=2",
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
kind: "flightNavigation",
|
|
bypassNavigationCache: true,
|
|
trace: decision.trace,
|
|
});
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.samePageSearch, {
|
|
targetHref: "https://example.com/docs?q=2",
|
|
});
|
|
});
|
|
|
|
it("treats a search change as a flight navigation even when the target adds a hash", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs?q=1",
|
|
targetHref: "https://example.com/docs?q=2#section",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: true });
|
|
});
|
|
|
|
it("classifies a cross-path navigation as a non-bypassing flight navigation", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs",
|
|
targetHref: "https://example.com/blog",
|
|
});
|
|
|
|
expect(decision).toEqual({
|
|
kind: "flightNavigation",
|
|
bypassNavigationCache: false,
|
|
trace: decision.trace,
|
|
});
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.crossDocumentFlight, {
|
|
targetHref: "https://example.com/blog",
|
|
});
|
|
});
|
|
|
|
it("treats search params that differ only by encoding as the same page", () => {
|
|
// "+" and "%20" both decode to a space, so this is not a search change and
|
|
// must not bypass the navigation cache. Guards the choice of comparing
|
|
// serialised params over raw search strings.
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs?a=+",
|
|
targetHref: "https://example.com/docs?a=%20",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: false });
|
|
});
|
|
|
|
it("refreshes an identical URL without replaying the current page response", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs?q=1",
|
|
targetHref: "https://example.com/docs?q=1",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: true });
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.samePageRefresh, {
|
|
targetHref: "https://example.com/docs?q=1",
|
|
});
|
|
});
|
|
|
|
it("refreshes an exact current URL when the committed snapshot is app-relative", () => {
|
|
const decision = classify({
|
|
basePath: "/app",
|
|
currentUrlSpace: "appRelativeSnapshot",
|
|
currentHref: "https://example.com/docs?q=1",
|
|
targetHref: "https://example.com/app/docs?q=1",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: true });
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.samePageRefresh, {
|
|
targetHref: "https://example.com/app/docs?q=1",
|
|
});
|
|
});
|
|
|
|
it("refreshes an identical non-empty hash", () => {
|
|
const decision = classify({
|
|
basePath: "/app",
|
|
currentUrlSpace: "appRelativeSnapshot",
|
|
currentHref: "https://example.com/docs#section",
|
|
targetHref: "https://example.com/app/docs#section",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: true });
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.samePageRefresh, {
|
|
targetHref: "https://example.com/app/docs#section",
|
|
});
|
|
});
|
|
|
|
it("does not strip a basePath-like first segment from an app-relative snapshot", () => {
|
|
const decision = classify({
|
|
basePath: "/docs",
|
|
currentUrlSpace: "appRelativeSnapshot",
|
|
currentHref: "https://example.com/docs/foo",
|
|
targetHref: "https://example.com/docs/docs/foo",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: true });
|
|
expectSingleTraceEntry(decision, NavigationTraceReasonCodes.samePageRefresh, {
|
|
targetHref: "https://example.com/docs/docs/foo",
|
|
});
|
|
});
|
|
|
|
it("refreshes an app-relative exact URL that preserves %20 spelling", () => {
|
|
const decision = classify({
|
|
basePath: "/docs",
|
|
currentUrlSpace: "appRelativeSnapshot",
|
|
currentHref: "https://example.com/docs/foo?q=%20",
|
|
targetHref: "https://example.com/docs/docs/foo?q=%20",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: true });
|
|
});
|
|
|
|
it("keeps %20 and + as distinct raw spellings for app-relative exact identity", () => {
|
|
const decision = classify({
|
|
basePath: "/docs",
|
|
currentUrlSpace: "appRelativeSnapshot",
|
|
currentHref: "https://example.com/docs/foo?q=%20",
|
|
targetHref: "https://example.com/docs/docs/foo?q=+",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: false });
|
|
});
|
|
|
|
it("treats hash removal as a same-document navigation", () => {
|
|
const decision = classify({
|
|
currentHref: "https://example.com/docs#section",
|
|
targetHref: "https://example.com/docs",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "sameDocumentScroll", hash: "" });
|
|
});
|
|
|
|
it("does not treat a cross-origin same-path hash target as a same-document scroll", () => {
|
|
const decision = classify({
|
|
currentHref: "https://a.example/docs?q=1",
|
|
targetHref: "https://b.example/docs?q=1#section",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: false });
|
|
});
|
|
|
|
it("does not treat a cross-origin same-path search target as same-page search", () => {
|
|
const decision = classify({
|
|
currentHref: "https://a.example/docs?q=1",
|
|
targetHref: "https://b.example/docs?q=2",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: false });
|
|
});
|
|
|
|
it("strips the base path before comparing pathnames for a hash change", () => {
|
|
const decision = classify({
|
|
basePath: "/app",
|
|
currentHref: "https://example.com/app/docs",
|
|
targetHref: "https://example.com/app/docs#section",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "sameDocumentScroll", hash: "#section" });
|
|
});
|
|
|
|
it("falls back to a non-bypassing flight navigation when an href cannot be parsed", () => {
|
|
const decision = classify({
|
|
currentHref: "not a url",
|
|
targetHref: "also not a url",
|
|
});
|
|
|
|
expect(decision).toMatchObject({ kind: "flightNavigation", bypassNavigationCache: false });
|
|
});
|
|
});
|