Skip to content

fix(e2e): stabilize test suite and fix blog revalidation regression - #420

Merged
ivogt merged 22 commits into
mainfrom
combined/parallel-loader-streaming
Mar 20, 2026
Merged

ivogt merged 22 commits into
mainfrom
combined/parallel-loader-streaming

Conversation

@ivogt

@ivogt ivogt commented Mar 19, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Stabilize the e2e test suite: 0 hard failures across router (1265+ tests) and demo app (187 tests)
  • Fix HMR test concurrency bug where route-types-hmr and intercept-hmr modified the same files simultaneously
  • Fix blog post revalidation regression where postRevalidation returned true for search-only navigations

Changes

Playwright config (router):

  • Workers: 6 → 3 to reduce dev server contention
  • Retries: 0 → 2 locally (matching CI)
  • Split hmr project → hmr-loader → hmr-routes → hmr-intercept chain to prevent concurrent file modifications
  • Decouple HMR/webkit from dev/production deps so flaky dev tests don't block 44+ HMR/webkit tests
  • Global expect timeout: 5s → 10s

Test file improvements:

  • Remove 43 hardcoded { timeout: 5000 } overrides that were lower than the new global default
  • Increase action settlement waits (600ms → 1500ms) and cache write waits (500ms → 1000ms)
  • Increase helper function default timeouts to 10s

Blog revalidation fix (tests/vite-rsc-demo/src/pages/blog.tsx):

  • isBlogProbeAction ?? defaultShouldRevalidate returned true for GET navigations with search param changes (undefined ?? true = true)
  • Fixed to explicitly check probe action OR slug change, preserving original search-param-skip behavior

Test plan

  • Router e2e: 0 failures, ~12 flaky (retry-recovered), 1265+ passed (2 consecutive runs)
  • Demo app: 0 failures, blog query-param revalidation test passes in both dev and production
  • Unit tests: 2620 passed
  • Typecheck, lint, format all pass

ivogt added 16 commits March 18, 2026 22:15
Parallel loaders (standalone segments keyed to parent shortCode) were
grouped with the layout in segmentTreeWalk. When the layout has no
loading(), renderSegments awaited them, blocking SSR for the full
loader duration.

Fix at the rendering layer with two small changes:

1. resolveParallelEntry: tag loader segments with parallelLoading
   (the owning parallel's loading fallback) when the parallel has
   loading() set.

2. renderSegments: in the "has loaders but no loading" branch, split
   loaders into layout-owned (await as before) and parallel-owned
   (attach as loaderDataPromise to the parallel segment so
   ParallelOutlet wraps in LoaderBoundary for streaming).

No changes to resolution paths, cache-hit paths, segment IDs,
partial navigation, or the segment tree walk. The fix is purely
in how renderSegments handles the tagged loaders.
Inflight prefetches buffer the entire response body (arrayBuffer) before
resolving. For routes with slow streaming loaders (e.g., parallel sidebar
with 5.5s loader), this blocks SPA navigation for the full loader
duration even though createFromFetch can consume streams incrementally.

When an inflight prefetch exists, skip it and do a fresh fetch instead.
The fresh fetch gives createFromFetch a streaming Response for
incremental consumption. The inflight prefetch completes in the
background and populates the cache for future navigations.
ctx.url is stripped of _rsc* params by stripInternalParams() in the
middleware pipeline, so _rsc_partial, _rsc_segments, _rsc_action,
and _rsc_loader checks on ctx.url were always false in production.

This caused:
- No segment hash in cache keys (different partial navigations to
  the same URL with different client segments shared a cache slot)
- _rsc_action and _rsc_loader skip guards never firing (loader
  requests going through the document cache instead of being bypassed)
- mayNeedSSR() receiving stripped URL, falling back to Accept header
  heuristic instead of definitive _rsc_partial detection

Fix: derive _rsc* detection and segment hashing from ctx.request.url
(the original, unstripped URL). ctx.url is still used for pathname
matching and user-facing query params.

Also update test mock to simulate production behavior: ctx.url has
_rsc* params stripped, ctx.request.url has them intact.
Prefetch fetch was calling response.arrayBuffer() to fully buffer the
body before resolving the inflight promise. This blocked navigation
for the entire stream duration when reusing inflight prefetches for
routes with slow streaming loaders.

Remove the arrayBuffer() call — return the streaming Response directly.
Cache storage uses response.clone() so the original body stays
consumable by navigation's createFromFetch for incremental streaming.
…d actions

- Add parallelLoaderSources to ResolvedSegment for stable aggregate
  promise identity across renders
- Fix segment reconciler to preserve parallel loader fields during
  partial updates
- Add restoreParallelLoaderMarkers for cache-hit/revalidation paths
  where parallelLoading tag is lost after serialization
- Add e2e blog test covering action revalidation with parallel sidebar
…euse

Prefetch fetch now tees the response body instead of buffering with
arrayBuffer(). One branch goes to cache storage, the other resolves
the inflight promise for navigation consumption. This lets navigation
reuse inflight prefetches without waiting for the full body to download.

Restores the inflight prefetch reuse path in navigation-client that
was previously removed — it now works correctly because the inflight
promise resolves with a streaming Response (not a buffered one).
…aming' into combined/parallel-loader-streaming
…ching, docs

Parallel entries are now keyed by slot name (Record<@slot, ParallelEntryData>)
instead of a flat array. This makes slot override semantics natural (record
key assignment = last-write-wins) and gives the resolution layer direct
slot→entry mapping.

- server/context.ts: ParallelEntries type (Partial<Record<@slot, ParallelEntryData>>),
  helper functions (getParallelEntries, getParallelSlotEntries, getParallelSlotCount)
- dsl-helpers.ts: parallel() creates one entry per slot, assigns to parent.parallel[slot]
- fresh.ts + revalidation.ts: iterate via getParallelSlotEntries(), dedup by entry ID
- segment-system.tsx: namespace-based restoreParallelLoaderMarkers and per-parallel
  loader isolation in renderSegments
- Fix stale-propagation test mock to include actual context exports

Docs:
- skills/parallel/SKILL.md: streaming behavior, slot override semantics
- execution-model.md: parallel streaming across all render paths
- feature-map.md: parallel slot streaming and override dedup
- feature-file-map.md: add segment-system.tsx and segment-reconciler.ts ownership
- semantic-change-checklist.md: parallel slots section with reconstruction invariant
…uning

- Split single `hmr` project into `hmr-loader` → `hmr-routes` → `hmr-intercept`
  chain to prevent concurrent file modifications (route-types-hmr was modifying
  route files while intercept-hmr expected them intact)
- Decouple HMR/webkit-smoke from dev/production dependencies — flaky dev tests
  no longer block 44+ HMR/webkit tests from running
- Set retries: 2 for both local and CI (was 0 locally)
- Set workers: 3 for both local and CI (was 6 locally) to reduce dev server
  contention that caused random test failures
…errides

- Set global expect timeout to 10s (was Playwright default 5s) to handle
  slower dev server responses under concurrent worker load
- Increase helper function default timeouts (waitForElement, clickAndWaitFor,
  waitForTextChange, waitForNumericChange) from 5s to 10s
- Remove explicit `{ timeout: 5000 }` from 43 positive assertions across
  8 flaky test files so they inherit the higher global default
- Increase pending-actions wait from 600ms to 1500ms for action settlement
- Increase handler-first cache write wait from 500ms to 1000ms
- Increase handle-breadcrumbs action timeout from 15s to 20s
- Increase timeout.test.ts waitForOnError default from 5s to 15s

Reduces flaky test count from ~17 to ~12 per run.
…d retries

- Increase waitForClientAgeToExceed polling timeout from 5s to 15s to
  handle slower timer increments under load
- Set retries: 2 for both local and CI (was 0 locally) to handle the
  flaky blog post query-param revalidation test
…ions

The blog probe action feature changed postRevalidation to use
`isBlogProbeAction ?? defaultShouldRevalidate` which falls through to
defaultShouldRevalidate (true) for non-action GET navigations with
search param changes. This caused the blog post to re-render on every
?tab=N navigation, resetting the client timer.

Fix: explicitly check for probe action OR slug change, preserving the
original behavior of skipping revalidation on search-only changes.
@pkg-pr-new

pkg-pr-new Bot commented Mar 19, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@rangojs/router@420

commit: 64d09b0

ivogt added 6 commits March 19, 2026 22:48
- Remove unused waitForCacheHit function from cache-status.test.ts
- Blog postRevalidation: defer to defaultShouldRevalidate for ALL
  actions (not just probe), only skip revalidation for GET navigations
  with same slug. This handles future non-probe actions correctly.
The hmr project was split into hmr-loader, hmr-routes, hmr-intercept
but the CI workflow still referenced the old 'hmr' project name,
causing CI to fail with "Project hmr not found".
…ze timing

- Remove clearLocalViteCache() and its call sites — isolated servers now
  use RANGO_E2E_VITE_CACHE_DIR env var instead of deleting shared cache
- Add default cacheDir fallbacks to vite configs (e2e-basic, e2e-timeout,
  test-app) so each app uses its own cache dir without the env var
- Add .vite* to .gitignore for isolated cache directories
- Small timing stabilization: waitForTimeout after hydration in cache
  and loader-behavior tests, expectNoReload guard in skipSSR test,
  increase prerender-intercept retry attempts from 2 to 5
After production eviction, Static() handler stubs lack a .handler
property, causing parallel slot resolution to skip (fresh path) or
crash with an invariant (revalidation path) before tryStaticSlot
could look up the pre-rendered component from __STATIC_MANIFEST.

Move tryStaticSlot before the handler undefined guard in fresh.ts,
and replace the invariant with a graceful fallback in revalidation.ts.
After `rm -rf node_modules/.vite`, the first HMR file change triggers
Vite dep re-optimization → full page reload, which destroys the
console event listener tests rely on. Add warmup tests that absorb
this initial reload so subsequent HMR tests get clean rsc:update events.
Isolated dev servers (use-cache, timeout tests) skip the shared
dev-warmup setup, so their first SSR request triggers Vite dep
optimization → module re-evaluation → in-memory cache loss. Add
warmupDevServer() that makes two consecutive SSR requests to absorb
the dep optimization cycle before tests run.

Also switch Unix kill() to process group kill (-pid) so Vite's child
processes (workerd, etc.) are cleaned up properly on teardown.
@ivogt
ivogt merged commit 9327b40 into main Mar 20, 2026
22 of 24 checks passed
@ivogt
ivogt deleted the combined/parallel-loader-streaming branch March 20, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant