Files
cloudflare__vinext/tests/app-request-context.test.ts
Nathan Nguyen 763736eae0 refactor(app-rsc-entry): delegate pure helpers to typed server modules (#1017)
* refactor(app-rsc-entry): delegate pure helpers to typed server modules

The generated RSC entry template contained five inline implementations of
pure runtime helpers: post-middleware request context construction,
RSC error handler creation, request context cleanup, RSC stream preload
hint normalization, and hook warning suppression. These inline blocks
repeated runtime behavior across multiple call sites and could not be
unit-tested independently.

Move each helper to a dedicated typed module under server/ so the
generated entry imports and wires them instead of owning the logic:
- buildPostMwRequestContext → server/app-post-middleware-context.ts
- createRscOnErrorHandler → server/app-rsc-error-handler.ts
- __clearRequestContext + setNavigationContext → server/app-request-context.ts
- renderToReadableStream preload hints → server/rsc-stream-hints.ts (createRscRenderer)
- hook warning console.error patch → server/app-hook-warning-suppression.ts

Each helper module has focused unit tests. The entry template is 55 lines
shorter and now describes the app shape; the typed modules own behaviour.

* fix: remove redundant as cast and fix test module caching

app-hook-warning-suppression.ts contained an `as string` cast that was
redundant because the typeof check on the prior line already narrows the
type. The hook warning suppression tests in tests 2-4 were passing
vacuously because dynamic import() returned the cached module whose
console.error patch was no longer active after test 1 restored the
original. Use vi.resetModules() before each test to force fresh module
evaluation.

clearAppRequestContext now delegates to setAppNavigationContext(null)
instead of calling the raw navigation and root-params setters directly,
matching the DRY pattern from the original inline code.

* fix: resolve entry helper rebase conflicts

* ci: rerun checks (font-google flake)

* Update packages/vinext/src/entries/app-rsc-entry.ts

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>

---------

Co-authored-by: ask-bonk[bot] <249159057+ask-bonk[bot]@users.noreply.github.com>
2026-05-03 17:34:06 +01:00

102 lines
3.0 KiB
TypeScript

import { describe, expect, it, afterEach } from "vite-plus/test";
import {
clearAppRequestContext,
setAppNavigationContext,
} from "../packages/vinext/src/server/app-request-context.js";
import { setHeadersContext, getHeadersContext } from "../packages/vinext/src/shims/headers.js";
import { getNavigationContext } from "../packages/vinext/src/shims/navigation.js";
import { getRootParam } from "../packages/vinext/src/shims/root-params.js";
describe("clearAppRequestContext", () => {
afterEach(() => {
// Ensure clean state for next test.
clearAppRequestContext();
});
it("nulls out all three per-request stores: headers, navigation, root params", () => {
// Seed all stores with non-null values.
setHeadersContext({
headers: new Headers({ host: "example.com" }),
cookies: new Map(),
});
setAppNavigationContext({
pathname: "/test",
searchParams: new URLSearchParams("q=1"),
params: { id: "42" },
});
clearAppRequestContext();
expect(getHeadersContext()).toBeNull();
expect(getNavigationContext()).toBeNull();
// Root params are cleared synchronously, so getRootParam resolves to undefined.
return getRootParam("id").then((val) => {
expect(val).toBeUndefined();
});
});
it("is idempotent — multiple calls do not throw", () => {
for (let i = 0; i < 3; i++) {
clearAppRequestContext();
}
expect(getHeadersContext()).toBeNull();
expect(getNavigationContext()).toBeNull();
});
});
describe("setAppNavigationContext", () => {
afterEach(() => {
clearAppRequestContext();
});
it("stores pathname, searchParams, and params so getNavigationContext can read them", () => {
const searchParams = new URLSearchParams("page=2");
setAppNavigationContext({
pathname: "/blog",
searchParams,
params: { slug: "hello", id: "42" },
});
const ctx = getNavigationContext();
expect(ctx).not.toBeNull();
expect(ctx!.pathname).toBe("/blog");
expect(ctx!.searchParams.get("page")).toBe("2");
expect(ctx!.params).toEqual({ slug: "hello", id: "42" });
});
it("clears both navigation context and root params when ctx is null", () => {
// Seed state.
setAppNavigationContext({
pathname: "/docs",
searchParams: new URLSearchParams(),
params: { slug: "readme" },
});
setAppNavigationContext(null);
expect(getNavigationContext()).toBeNull();
return getRootParam("slug").then((val) => {
expect(val).toBeUndefined();
});
});
it("clearing navigation does not affect the headers context", () => {
setHeadersContext({
headers: new Headers({ host: "example.com" }),
cookies: new Map([["a", "1"]]),
});
setAppNavigationContext({
pathname: "/a",
searchParams: new URLSearchParams(),
params: {},
});
setAppNavigationContext(null);
// Headers should survive the navigation clear — they're independent stores.
const h = getHeadersContext();
expect(h).not.toBeNull();
expect(h!.headers.get("host")).toBe("example.com");
});
});