Repository navigation
Description
Summary
Three correctness fixes from the multi-agent review landed with unit coverage but still lack the dev + production e2e fixtures the repo mandates (CLAUDE.md: "All e2e tests MUST cover BOTH dev AND production modes"). This issue tracks adding them. The fixtures below are already written and verified to compile; they were pulled out of the stabilization PR because regenerating the test-app .gen.ts + running dual-mode Playwright is a separate, heavier step.
Gap 1 — Prerender Passthrough + loading() + ctx.passthrough() (was M16)
Consumer story. A consumer pre-renders a route, declares a loading() skeleton, and skips specific params at build with ctx.passthrough():
// urls.tsx — what a consumer writes
path("/:slug", Passthrough(ProductDef, LiveProduct), { name: "product" }, () => [
loading(<ProductSkeleton />), // <- the loading() boundary is the trigger
])
// ProductDef build handler
async (ctx) => {
if (!(await isPublished(ctx.params.slug))) return ctx.passthrough(); // skip baking
return <Product slug={ctx.params.slug} />;
}Before the fix: because loading() defers the build handler, ctx.passthrough() arrived as a Promise-wrapped component and the synchronous sentinel check missed it — the build baked a corrupt artifact for the skipped param instead of deferring to the live handler. After the fix (detectPrerenderPassthrough awaits the thenable), the param correctly passes through. Unit test: prerender-passthrough-detect.test.ts.
Fixture to add (e2e/test-app/src/urls/prerender-loading.tsx):
import { urls, Prerender, Passthrough } from "@rangojs/router";
export const PrerenderLoadingDef = Prerender<{ slug: string }>(
async () => [{ slug: "baked" }, { slug: "skip" }],
async (ctx) => {
if (ctx.params.slug === "skip") return ctx.passthrough();
return (
<div data-testid="prerender-loading-page">
<h1 data-testid="prerender-loading-title">{ctx.params.slug}</h1>
<p data-testid="prerender-loading-build">{String(ctx.build)}</p>
<p data-testid="prerender-loading-ts">{Date.now()}</p>
</div>
);
},
);
export const PrerenderLoadingTest = Passthrough(PrerenderLoadingDef, async (ctx) => (
<div data-testid="prerender-loading-page">
<h1 data-testid="prerender-loading-title">{ctx.params.slug}</h1>
<p data-testid="prerender-loading-build">{String(ctx.build)}</p>
<p data-testid="prerender-loading-live">live</p>
<p data-testid="prerender-loading-ts">{Date.now()}</p>
</div>
));
export const prerenderLoadingPatterns = urls(({ path, loading }) => [
path("/:slug", PrerenderLoadingTest, { name: "detail" }, () => [
loading(<div data-testid="prerender-loading-skeleton">Loading...</div>),
]),
]);Wire into urls.tsx: include("/prerender-loading", prerenderLoadingPatterns, { name: "prerenderLoading" }).
Assertions (dev + production):
baked→ prerendered (ctx.buildtrue at build; served from manifest).skip→ passes through to the live handler (prerender-loading-livepresent;ctx.buildfalse).- production:
/__test/prerender-manifest-entries?route=prerenderLoading.detailcount includesbakedbut notskip(the M16 regression guard — without the fix,skipwould be baked).
Gap 2 — redirect() from a loading() route (was M9)
Consumer story. A sync handler return of redirect() on a loading() route short-circuits to a real HTTP redirect (supported); an async handler return is streamed instead (documented limitation — dev warns via warnOnStreamedResponse).
// supported: synchronous return redirects
path("/sync", () => redirect("/loading-redirect/target"), { name: "sync" }, () => [
loading(<Skeleton />),
])Fixture to add (e2e/test-app/src/urls/loading-redirect.tsx):
import { urls, redirect } from "@rangojs/router";
export const loadingRedirectPatterns = urls(({ path, loading }) => [
path("/sync", () => redirect("/loading-redirect/target"), { name: "sync" }, () => [
loading(<div data-testid="lr-skeleton">Loading...</div>),
]),
path("/target", () => <div data-testid="lr-target">arrived</div>, { name: "target" }),
]);Assertions (dev + production): navigating to /loading-redirect/sync lands on /loading-redirect/target (lr-target visible). (The async-swallow limitation is intentionally not e2e'd — it produces a degraded render, not a navigable outcome; it's covered by the unit test + execution-model.md.)
Additional e2e gaps surfaced by the review (lower priority)
- Build-time intercept
_currentSegmentIdnot pinned →ctx.usehandle data dropped from the intercept prerender artifact. Needs a test-app intercept handler that pushes a handle, asserted dev + prod. server-action-bridgepre-dispatch failure over-invalidates the client cache + broadcasts staleness even though nothing reached the server (over-invalidation only — safe today).- document-cache
private, s-maxagewrites to the shared store (spec-correct per RFC 7234 but a defense-in-depth gap);document-cache.test.ts:268coversprivatealone.
Test plan
- Wire both fixtures into
e2e/test-app/src/urls.tsx; regenerate.gen.tsvia the test-app build/dev (runtime discovery — the static CLI can't resolve the factory include); commitrouter.named-routes.gen.ts. - Add
prerender-loading.test.tsandloading-redirect.test.tswith the assertions above, dev +(production)describes (useprodDescribe/the bucketing convention). - Run both in dev and production locally; confirm green.
Activity
ivogt commentedon Jun 13, 2026
What can move down the pyramid (re: the new
@rangojs/router/testingprimitives +docs/testing.md)Checked each gap against the testing-surface table. Most have a unit-coverable slice; only the server/Flight/build-lifecycle parts genuinely need e2e.
loading()prerender-passthrough-detect.test.ts)Prerenderis e2e(prod) +assertCacheStatus(…, "prerendered")(guide line 70)loading()return redirect()throws theResponse→renderHandlercan assert the thrown redirect; dev warning already unit-tested (warnOnStreamedResponse)loading()deferral path (async handler streamed, not redirected) runs throughfresh.tsresolution → e2e_currentSegmentIdhandle pinningcollectHandle; seeded read →renderRoute({ handles })(guide lines 58-59)_currentSegmentIdpinning during prerender → e2efinalizeAction/markCacheAsStaleAndBroadcastlogic is a navigation-store unit test (cf. existingnavigation-store.test.ts) — no e2e neededuseAction) —renderRoutebypasses it (guide line 83) → e2eprivate, s-maxageparseCacheControlis a pure fn — export it (or the cache-decision fn) and unit-test the decision directlyNet: 4 of 5 gaps gain a unit/integration slice; the irreducible e2e remainder is the prerender build→serve round-trip, the
loading()redirect deferral, and the client action lifecycle — each needs the real server/Flight/navigation lifecycle that the unit primitives "deliberately don't cover" (guide's traps table).Quick wins to land first (cheap, no server):
parseCacheControldecision unit test (export the fn).renderHandlertest for syncreturn redirect()(asserts the thrownResponse).navigation-storeunit test for pre-dispatch failure → no stale-broadcast.These shrink the e2e surface to just the genuinely-irreducible cases above.
fix(router): stabilize multi-agent review findings (15 mediums + clea…
fix(router): review follow-ups — #567 dual-mode e2e, #569 pre-1.0 API…
fix(router): matrix-critical review follow-ups — #568 trie-suffix, #569…
ivogt commentedon Jun 14, 2026
Re-closing: verified complete on main. Both mandated dual-mode e2e fixtures (M16 prerender+loading+passthrough, M9 sync redirect under loading) shipped in #573, and the 3 lower-priority gaps (build-time intercept _currentSegmentId, server-action pre-dispatch over-invalidation, document-cache private/no-store veto) shipped in #575. (The earlier close was deliberate, not an accidental one.)