Files
cloudflare__vinext/tests/app-page-params.test.ts
T
Nathan Nguyen a35e43cef3 feat(skip): add layout safety observation foundations (#1672)
* feat(skip): add layout safety observation foundations

Introduce the per-layout observation primitives that later slices need
to decide whether retained static layouts can be reused. This slice
adds the machinery only; nothing reads the observations yet, so
behavior is unchanged.

- app-layout-param-observation: per-layout completeness, param keys,
  structural scope, finite revalidate, request API, cacheLife, cache
  tag, cacheable fetch, dynamic fetch, and unstable_cache observation
  tracker.
- app-page-probe: bounded React server subtree probe that walks
  layout-returned children including memo, forwardRef, and React.lazy
  wrappers, enforces depth and node limits, and refuses single-use
  iterable children.
- shims: thenable params observer for sync, await, destructure, and
  enumeration. Cache, fetch, and unified request context record
  unstable_cache, cacheable fetch, and request-scoped observation
  state.
- app-page-params: structural segment-param scope so empty optional
  catch-all targets are not treated as param-free downstream.

No skip transport behavior. Later slices wire the planner, manifest,
and encoder onto these observations.

* fix(skip): address review feedback on observation foundations

- Add currentFetchSoftTags reset in runWithIsolatedProbeDependencies to prevent soft tag leak from parent scope (matches runWithFetchCache isolation)
- Replace unnecessary JSON.stringify in cacheLife check with direct null comparison
- Add comment documenting intentional omission of markProbeComplete on probe errors (keeps completeness as unknown, safe fallback to render-and-send)

* fix(skip): record cacheable-fetch observation synchronously before first await

The patched fetch deferred observation and tag collection past
await buildFetchCacheKey(). If a layout probe started a cacheable
fetch and returned synchronously without awaiting it, the probe
snapshot would see cacheableFetchCount: 0 -- a false proof that
could let the skip planner reuse a stale retained layout.

Move observation and tag recording before the first await in the
cacheable branch. If key generation later fails and the fetch falls
back to dynamic, recording both is conservative (false unsafe costs
performance, not correctness).

Add an integration test that exercises the real patched fetch shim
without mocks to verify synchronous observation capture.

* fix(test): remove polluting fetch restore and catch background promise

Do not restore globalThis.fetch in test cleanup. ensureFetchPatch
sets a global _PATCH_KEY guard; restoring fetch without clearing
that marker makes later calls skip reinstallation silently.

Catch the background fetch promise inside the probe so the patched
fetch continuation (cache lookup, network fetch) cannot produce an
unhandled rejection after the synchronous assertions complete.

* chore: empty commit to trigger CI

* chore: empty commit to trigger CI
2026-05-31 11:39:47 +01:00

58 lines
2.6 KiB
TypeScript

import { describe, expect, it } from "vite-plus/test";
import {
resolveAppPageSegmentParamScopeKeys,
resolveAppPageSegmentParams,
} from "../packages/vinext/src/server/app-page-params.js";
describe("app page params helpers", () => {
it("passes only params that apply to each layout", () => {
// Ported from Next.js: test/e2e/app-dir/layout-params/layout-params.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/layout-params/layout-params.test.ts
const routeSegments = ["base", "[param1]", "[param2]"];
const matchedParams = { param1: "something", param2: "another" };
expect(resolveAppPageSegmentParams(routeSegments, 0, matchedParams)).toEqual({});
expect(resolveAppPageSegmentParams(routeSegments, 1, matchedParams)).toEqual({});
expect(resolveAppPageSegmentParams(routeSegments, 2, matchedParams)).toEqual({
param1: "something",
});
expect(resolveAppPageSegmentParams(routeSegments, 3, matchedParams)).toEqual({
param1: "something",
param2: "another",
});
});
it("scopes catch-all params to the catch-all layout", () => {
// Ported from Next.js: test/e2e/app-dir/layout-params/layout-params.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/layout-params/layout-params.test.ts
const routeSegments = ["catchall", "[...params]"];
const matchedParams = { params: ["something", "another"] };
expect(resolveAppPageSegmentParams(routeSegments, 1, matchedParams)).toEqual({});
expect(resolveAppPageSegmentParams(routeSegments, 2, matchedParams)).toEqual({
params: ["something", "another"],
});
});
it("omits empty optional catch-all params from layouts", () => {
// Ported from Next.js: test/e2e/app-dir/layout-params/layout-params.test.ts
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/layout-params/layout-params.test.ts
const routeSegments = ["optional-catchall", "[[...params]]"];
expect(resolveAppPageSegmentParams(routeSegments, 1, { params: [] })).toEqual({});
expect(resolveAppPageSegmentParams(routeSegments, 2, { params: [] })).toEqual({});
expect(
resolveAppPageSegmentParams(routeSegments, 2, { params: ["something", "another"] }),
).toEqual({
params: ["something", "another"],
});
});
it("keeps optional catch-all names in structural layout param scope", () => {
const routeSegments = ["docs", "[[...slug]]"];
expect(resolveAppPageSegmentParams(routeSegments, 2, { slug: [] })).toEqual({});
expect(resolveAppPageSegmentParamScopeKeys(routeSegments, 2)).toEqual(["slug"]);
});
});