Skip to content

test: E2E verification for layout persistence flat payload pipeline#752

Merged
james-elicx merged 6 commits intocloudflare:mainfrom
NathanDrake2406:feat/layout-persistence-pr-3-verification
Apr 10, 2026
Merged

test: E2E verification for layout persistence flat payload pipeline#752
james-elicx merged 6 commits intocloudflare:mainfrom
NathanDrake2406:feat/layout-persistence-pr-3-verification

Conversation

@NathanDrake2406
Copy link
Copy Markdown
Contributor

Summary

Part of #726 (PR 3). Pure test code — zero production changes. Proves the flat keyed map payload pipeline from PR 2c actually works end-to-end in a real browser.

Every test answers: "would a real user notice if this broke?" Layout state surviving navigation, template state resetting on segment change, errors clearing on re-navigation — these are observable user behaviors, not implementation details.

What's tested

Behavior Method
Layout state persists across sibling navigation Client counter in dashboard layout survives dashboard → settings → dashboard
Layout state resets on hard navigation page.goto() resets counter vs Link click preserving it
Template remounts on segment boundary change Root template counter resets on //about (segment changes)
Template persists within same segment Root template counter survives /dashboard/dashboard/settings (same top-level segment)
Error clears on navigate-away-and-back Trigger error → client-nav to home → client-nav back → fresh page
Back/forward preserves layout state Counter survives goBack() / goForward() through dashboard history
Parallel slots persist on soft nav @team and @analytics slot content survives /dashboard/dashboard/settings
Parallel slots show default on hard nav Direct load of /dashboard/settings renders default.tsx instead of page slot content

What's NOT tested here (and why)

  • Root layout switch — requires restructuring fixtures into route groups with separate root layouts. Already unit-tested via shouldHardNavigate in tests/app-browser-entry.test.ts
  • RSC payload structure — implementation detail, covered by unit tests in PR 2c
  • Reducer dispatch types — wiring, not user-observable behavior

Fixture changes

Minimal additions to tests/fixtures/app-basic/ to make behaviors observable:

  • app/components/layout-counter.tsx"use client" counter proving layout persistence
  • app/components/template-counter.tsx"use client" counter proving template remount
  • app/dashboard/layout.tsx — added LayoutCounter + nav links between dashboard pages
  • app/template.tsx — added TemplateCounter
  • app/error-test/error.tsx — added "Go home" Link for error recovery navigation
  • app/page.tsx — added "Error Test" Link

All existing E2E tests (navigation, error-handling, navigation-flows — 25 tests) still pass with these fixture changes.

Test plan

  • tests/e2e/app-router/layout-persistence.spec.ts — 8 E2E tests, all passing
  • Existing navigation/error-handling/navigation-flows E2E — 25 tests, no regressions
  • tests/app-router.test.ts -t "not-found|forbidden|unauthorized" — 5 SSR integration tests, all passing
  • vp check — 0 lint/type errors

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 2, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@752

commit: 3bb92cd

@NathanDrake2406 NathanDrake2406 marked this pull request as draft April 2, 2026 06:58
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review April 4, 2026 06:36
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

ask-bonk[bot]
ask-bonk bot previously requested changes Apr 4, 2026
Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description is misleading — this is not "pure test code"

The title and summary claim "Pure test code — zero production changes" but this PR modifies 11 production source files (3 entirely new) across the entire App Router rendering pipeline:

  • app-rsc-entry.ts — rewrites the generated entry to produce flat element maps instead of nested ReactNode trees
  • app-browser-entry.ts — replaces useState with useReducer, adds hard-navigation detection, changes how all navigation dispatches work
  • app-ssr-entry.ts — rewrites VinextFlightRoot to consume AppWireElements via ElementsContext
  • app-page-route-wiring.tsx — complete rewrite from nested element tree to flat keyed map
  • slot.tsx — removes React.use() for Promises, switches to synchronous context, rewrites merge logic
  • app-router.ts — adds templateTreePositions to route scanning
  • app-page-boundary-render.ts — integrates elements map into error/access-fallback rendering
  • app-page-render.ts — widens type signatures
  • app-browser-state.ts (NEW) — router reducer, hard-navigate detection, pending commit helpers
  • app-elements.ts (NEW) — elements map types, normalization, metadata extraction
  • app-render-dependency.tsx (NEW) — render ordering mechanism via promise-based dependency gates

This is a fundamental architectural change — the shift from a single nested ReactNode tree to a flat keyed AppElements map for the RSC payload. That's a legitimate and valuable change, but the PR description should accurately reflect it. As-is, reviewers scanning the title would skip the production code review entirely.

Please update the PR title and description to accurately reflect the scope of production changes. If this was originally meant to be split from another PR (the summary mentions "PR 2c"), the production changes should be in their own PR or the description should clearly enumerate them.


Code-level feedback

The architectural direction (flat element map, reducer-based state, hard-navigate detection) looks sound. The E2E tests are well-written. Below are specific issues I found in the production code.

};
}

export async function applyAppRouterStateUpdate(options: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

applyAppRouterStateUpdate is exported but never called from production code — it's only used in tests/app-browser-entry.test.ts. If this is intended as a public helper for future use, that's fine, but consider adding a comment indicating it's part of the planned API. If it's test-only scaffolding, it shouldn't live in the production module.

Alternatively, inline the test logic directly — the test already imports createPendingNavigationCommit and shouldHardNavigate separately, which is all the function composes.

Comment on lines +336 to +338
const stateRef = useRef(treeState);
stateRef.current = treeState;
browserRouterStateRef = stateRef;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assigning to stateRef.current during render (line 337) is fine — it's a ref update, not a side effect. But assigning to the module-level browserRouterStateRef (line 338) during render is a side effect that runs on every render, not just mount. Under React Strict Mode or concurrent features, this could run multiple times for the same render.

The comment says "not in an effect — so there is no stale window" but this trades correctness for timing. In Strict Mode dev, React intentionally double-invokes render to catch impure renders — this assignment will run twice per render, though it happens to be idempotent (same ref object).

Consider moving the module-level assignment into the useLayoutEffect alongside the dispatch assignment, or document why the Strict Mode double-execution is acceptable.

segment.startsWith("[[...") &&
segment.endsWith("]]") &&
segment.length >= "[[...x]]".length
segment.length > "[[...x]]".length - 1
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change from >= "[[...x]]".length to > "[[...x]]".length - 1 is functionally equivalent (>= 8 vs > 7) but harder to reason about. The original was clearer: "the segment must be at least as long as the shortest valid optional catch-all". The new form inverts the comparison direction and subtracts one, which reads like a bug fix but isn't.

If this was changed to fix a linting rule or formatter preference, a comment explaining why would help. Otherwise, prefer the original form:

Suggested change
segment.length > "[[...x]]".length - 1
segment.length >= "[[...x]]".length

children: ReactNode,
dependency: AppRenderDependency,
): ReactNode {
function ReleaseAppRenderDependency() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReleaseAppRenderDependency calls dependency.release() during render (not in an effect). This works for server-side rendering (RSC) where components render once, top-to-bottom. But if this component is ever rendered in a client environment (or under React Strict Mode), React could invoke it multiple times for the same render pass. The released guard in createAppRenderDependency prevents double-resolution, so it's safe in practice, but calling side effects during render is unconventional.

Worth a brief comment explaining this is intentional and RSC-only.

pending.rootLayoutTreePath,
false,
);
return result;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the server action result is NOT a ServerActionResult (the else branch), this returns result — which is the raw AppWireElements object. The caller of the server action callback likely expects either undefined or a return value, not the entire wire-format elements map.

The isServerActionResult branch returns result.returnValue.data or undefined, which is correct. But this fallback branch leaks internal transport data back to the caller. Was this intentional? The previous code had the same pattern, but now result is typed as AppWireElements rather than ReactNode, making the leak more obvious.

}
}

export function shouldHardNavigate(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When either currentRootLayoutTreePath or nextRootLayoutTreePath is null, this returns false (no hard navigate). This means:

  • First page load (current = null) → always soft navigate (correct)
  • Route with no root layout (next = null) → always soft navigate

But what about navigating FROM a route with a root layout TO a route without one (current = "/", next = null)? That would be a root layout removal, which arguably should hard-navigate. And the reverse (current = null, next = "/") would be a root layout addition.

Is this intentional? If so, a comment explaining the null semantics would help. If not, this could be a subtle bug where layout-less routes silently soft-navigate when they shouldn't.

export type AppElementValue = ReactNode | typeof UNMATCHED_SLOT | string | null;
export type AppWireElementValue = ReactNode | string | null;

export type AppElements = Readonly<Record<string, AppElementValue>>;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Readonly<Record<string, ...>> type is a good start, but it allows any string key. Consider whether a branded/tagged type would prevent accidental misuse — e.g., passing a random Record<string, ReactNode> where AppElements is expected without the required __route and __rootLayout keys. The runtime validation in readAppElementsMetadata catches this, but a compile-time guard would be stronger.

Not blocking, just a suggestion for future hardening.

}

void main();
if (typeof document !== "undefined") {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good addition — guarding main() behind typeof document !== "undefined" prevents execution in non-browser environments (e.g., when the module is imported for type-checking or testing). This is a nice improvement over the previous unconditional void main().

@@ -0,0 +1,228 @@
import { test, expect } from "@playwright/test";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The E2E tests are well-structured and test genuinely user-observable behavior. The table in the PR description mapping behaviors to test methods is helpful. A few observations:

  1. The waitForHydration helper using .toPass() with retry is a good pattern for flaky-resistant hydration checks.
  2. The error recovery test (line 120-123) wraps click + expect in .toPass() which handles the timing gap between click and error boundary rendering nicely.
  3. Consider adding a test for the hard-navigate detection path — e.g., if route groups with different root layouts exist in the fixture, navigating between them should trigger a full page load (verifiable by checking window.__VINEXT_HYDRATED_AT changes).

Comment on lines +285 to +289
export function buildAppPageElements<
TModule extends AppPageModule,
TErrorModule extends AppPageErrorModule,
>(options: BuildAppPageElementsOptions<TModule, TErrorModule>): AppElements {
const elements: Record<string, ReactNode | string | null> = {};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is ~300 lines long and manages multiple parallel data structures (6+ Maps, multiple dependency arrays). While the logic is correct, the complexity is high. Consider extracting the dependency-graph construction (lines 306-343) into a separate function — it's a self-contained concern and would make the main function easier to follow.

Not blocking, but this is the kind of function that's easy to introduce bugs into during future maintenance.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 4, 2026

Review posted successfully. Here's a summary of my review:

Requested changes on PR #752 with 10 inline comments covering:

Primary concern: Misleading PR description

The PR claims "Pure test code — zero production changes" but actually modifies 11 production source files (3 entirely new), implementing a fundamental architectural shift from a single nested ReactNode tree to a flat keyed AppElements map for the RSC payload. The description needs to accurately reflect this scope.

Code-level feedback (10 inline comments):

  1. applyAppRouterStateUpdate is dead code — exported but never used in production, only in tests
  2. Module-level side effect during renderbrowserRouterStateRef = stateRef runs every render, not just mount; may interact poorly with Strict Mode
  3. Unnecessary refactor of length check>= 8 changed to > 7 with no functional difference but reduced clarity
  4. Side effect in render for ReleaseAppRenderDependency — works for RSC but unconventional; needs a comment
  5. Server action leaking AppWireElements to caller — the non-ServerActionResult branch returns raw wire elements instead of undefined
  6. shouldHardNavigate null semantics — navigating between routes with and without root layouts (one null, one non-null) silently soft-navigates; needs documentation or a fix
  7. AppElements type could be strongerRecord<string, ...> doesn't enforce required __route/__rootLayout keys at compile time
  8. Good: document guard on main() — nice improvement
  9. Good: E2E tests are well-structured — suggested adding a hard-navigate detection test
  10. buildAppPageElements complexity — ~300 lines with 6+ Maps; dependency graph construction could be extracted

github run

@NathanDrake2406 NathanDrake2406 marked this pull request as draft April 4, 2026 13:21
@NathanDrake2406 NathanDrake2406 force-pushed the feat/layout-persistence-pr-3-verification branch from efebde9 to 490a5b2 Compare April 10, 2026 07:47
@NathanDrake2406 NathanDrake2406 marked this pull request as ready for review April 10, 2026 14:33
Prove the flat keyed map architecture works end-to-end:
- Layout state persists across sibling navigation (counter survives)
- Template remounts on segment boundary change, persists within segment
- Error boundary clears on navigate-away-and-back
- Back/forward preserves layout state through history
- Parallel slots persist on soft nav, show default.tsx on hard nav

Zero production code changes — test fixtures and Playwright specs only.
@NathanDrake2406 NathanDrake2406 force-pushed the feat/layout-persistence-pr-3-verification branch from ceca050 to 7a73a1d Compare April 10, 2026 14:43
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk review

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This is a well-structured test PR. The 8 E2E tests cover genuinely user-observable behaviors (layout persistence, template remount semantics, error recovery, history navigation, parallel slot behavior), and the fixture additions are minimal and purposeful. All CI checks pass.

Correction on the prior review: The previous review claimed this PR modifies 11 production source files — that is incorrect. This PR changes exactly 8 files, all under tests/. The production code comments in that review (about app-browser-state.ts, app-page-route-wiring.tsx, app-render-dependency.tsx, etc.) are about files that do not appear in this diff. Those may be from a related PR (#726 / PR 2c) but are not part of this one.

A few observations below — nothing blocking.

expect(after).toBeGreaterThan(before);
}).toPass({ timeout: 10_000 });

return readCounterValue(page, controls);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good hydration-gating pattern — polling click → read → assert inside .toPass() elegantly handles the timing gap between SSR render and React hydration.

One thing to be aware of: each .toPass() retry clicks the button, so if hydration completes mid-retry-loop, the counter could have been incremented multiple times. The tests handle this correctly by capturing the return value and using relative offsets (initialCount + 1, initialCount + 2) rather than assuming initialCount === 0. Nice.

Comment on lines +291 to +292
await expect(page.locator('[data-testid="team-slot"]')).not.toBeVisible();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeVisible();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor inconsistency: in the soft-nav test (lines 274-275), you use not.toBeAttached() to assert the default slots are absent from the DOM. Here you use not.toBeVisible() to assert the page-specific slot content is absent after a hard load.

On a hard load to /dashboard/settings, the server renders default.tsx for these slots — the page-specific slot content (team-slot, analytics-slot) shouldn't be in the DOM at all, not just hidden. not.toBeAttached() would be the stronger assertion and consistent with the pattern used elsewhere in this file.

Suggested change
await expect(page.locator('[data-testid="team-slot"]')).not.toBeVisible();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeVisible();
await expect(page.locator('[data-testid="team-slot"]')).not.toBeAttached();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeAttached();

await expect(page.locator("#error-boundary")).toBeVisible({ timeout: 10_000 });

// Client-navigate away to home via the link in the error boundary
await page.click('[data-testid="error-go-home"]');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test nicely complements the existing error-interactive.spec.ts "navigating away from error page works" test (line 95), which uses page.goto() (hard nav) to leave the error. Here you test the more interesting case: client-side nav away and back, proving the error boundary clears. Good coverage gap to fill.

// ---------------------------------------------------------------------------

test.describe("Error recovery across navigation", () => {
test("navigating away from error and back clears the error", async ({ page }) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider whether this test should also suppress the Vite dev error overlay, similar to how error-interactive.spec.ts does with disableViteErrorOverlay(). When an error is thrown, Vite's dev overlay can appear and intercept pointer events, which could make the error-go-home link click flaky. The test passes in CI today, but the overlay behavior can be timing-dependent.

If you'd rather not add the helper dependency, at minimum you could add a brief comment explaining why it's not needed here (e.g., the Link click happens inside the error boundary which renders above the overlay, or the overlay doesn't appear for caught errors).

Settings
</Link>
</nav>
<LayoutCounter />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good placement — putting LayoutCounter directly in the dashboard layout (not in a page) is exactly right for testing layout persistence. If it were in a page component, it would remount on every navigation regardless.

import { test, expect } from "@playwright/test";
import { waitForAppRouterHydration } from "../helpers";

const BASE = "http://localhost:4174";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: other E2E specs in this directory also hardcode http://localhost:4174, so this is consistent. But since the Playwright config already sets baseURL for the app-router project, you could use relative URLs (just /dashboard instead of ${BASE}/dashboard) and Playwright will resolve them against the project's baseURL. That would make these tests resilient to port changes without code edits.

Not blocking since the existing convention is to use the hardcoded base.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 10, 2026

Review posted successfully. Here's a summary of what I submitted:

Overall assessment: This is a solid test-only PR. The 8 E2E tests are well-structured, testing user-observable behaviors rather than implementation details. All CI checks pass. The fixture additions are minimal and well-scoped.

Key correction: The previous review by ask-bonk was hallucinated — it claimed 11 production source files were modified and posted 10 inline comments about files (app-browser-state.ts, app-page-route-wiring.tsx, app-render-dependency.tsx, etc.) that are not in this diff at all. This PR touches only 8 files, all under tests/.

Inline comments posted (6):

  1. Line 43 — Acknowledged the smart waitForInteractiveCounter pattern that handles hydration timing gracefully with relative offsets
  2. Lines 291-292 — Suggested changing not.toBeVisible() to not.toBeAttached() for consistency with the rest of the file and stronger assertion semantics (with code suggestion)
  3. Line 197 — Noted this nicely complements the existing hard-nav error test in error-interactive.spec.ts
  4. Line 184 — Flagged potential flakiness from Vite's dev error overlay (similar to the disableViteErrorOverlay helper used in error-interactive.spec.ts)
  5. Line 25 of dashboard/layout.tsx — Confirmed correct placement of LayoutCounter in the layout rather than a page
  6. Line 4 — Nit about hardcoded BASE URL vs using Playwright's baseURL (not blocking, matches existing convention)

github run

@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk re-review for issues. any blockers?

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review

This is a clean, well-structured test PR. The 8 E2E tests cover genuinely user-observable behaviors and the fixture additions are minimal and well-scoped. The PR description accurately reflects the scope — this is test-only code.

Two actionable items (one potential flakiness issue, one assertion inconsistency), plus a couple of minor observations.

await page.getByTestId("trigger-error").click();

// Error boundary should be visible
await expect(page.locator("#error-boundary")).toBeVisible({ timeout: 10_000 });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't suppress the Vite dev error overlay, unlike error-interactive.spec.ts which uses disableViteErrorOverlay() before triggering errors. When the error is thrown, Vite's dev overlay can appear and intercept pointer events on the error-go-home link at line 197.

The test passes in CI today, but this is a known source of flakiness in this repo — error-interactive.spec.ts added that helper specifically to solve flaky click failures. I'd recommend either:

  1. Importing and calling disableViteErrorOverlay(page) before triggering the error, or
  2. Adding a comment explaining why it's not needed here (e.g., the error boundary's Link renders above the overlay, or .toPass() retries handle it)

Since the error-go-home click at line 197 is a bare page.click() with no retry wrapper, this seems like the more fragile path compared to error-interactive.spec.ts.

Comment on lines +291 to +292
await expect(page.locator('[data-testid="team-slot"]')).not.toBeVisible();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeVisible();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent assertion: the soft-nav test at lines 274-275 uses not.toBeAttached() (element not in DOM at all), but the hard-nav test here uses not.toBeVisible() (element may be in DOM but hidden).

On a hard load to /dashboard/settings, the server renders default.tsx for these slots — the page-specific team-slot / analytics-slot content shouldn't be in the DOM at all, making not.toBeAttached() the stronger and more correct assertion. It would also be consistent with the pattern 16 lines above.

Suggested change
await expect(page.locator('[data-testid="team-slot"]')).not.toBeVisible();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeVisible();
await expect(page.locator('[data-testid="team-slot"]')).not.toBeAttached();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeAttached();

Comment on lines +36 to +43
await expect(async () => {
const before = await readCounterValue(page, controls);
await incrementButton.click();
const after = await readCounterValue(page, controls);
expect(after).toBeGreaterThan(before);
}).toPass({ timeout: 10_000 });

return readCounterValue(page, controls);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a well-designed hydration gate. Polling click → read → assert inside .toPass() handles the timing gap between SSR render and React hydration without hardcoded waits.

Worth noting: each retry iteration clicks the button, so if hydration completes mid-loop the counter will have been incremented multiple times. The callers handle this correctly by capturing the return value and using relative offsets (initialCount + 1, initialCount + 2) rather than assuming initialCount === 0. Good pattern.

await expect(page.locator("#error-boundary")).toBeVisible({ timeout: 10_000 });

// Client-navigate away to home via the link in the error boundary
await page.click('[data-testid="error-go-home"]');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice complement to the existing error-interactive.spec.ts "navigating away from error page works" test (line 95 in that file), which uses page.goto() (hard nav) to leave the error page. This tests the more interesting case: client-side nav away and back, proving the error boundary resets. Good coverage gap to fill.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 10, 2026

Review posted. Here's a summary of what I submitted:

Overall: Clean, well-structured test-only PR. 8 E2E tests covering user-observable behaviors with minimal fixture additions. The PR description is accurate.

Two actionable items:

  1. Potential flakiness in error recovery test (line 194) — The test triggers a client error but doesn't suppress the Vite dev error overlay, unlike error-interactive.spec.ts which uses disableViteErrorOverlay() for exactly this reason. The error-go-home link click at line 197 is a bare page.click() with no retry wrapper, making it vulnerable to the overlay intercepting pointer events.

  2. Inconsistent assertion in parallel slots hard-nav test (lines 291-292) — Uses not.toBeVisible() to assert slot content is absent, but the soft-nav test 16 lines above uses not.toBeAttached(). On a hard load, the page-specific slot content shouldn't be in the DOM at all, so not.toBeAttached() is the stronger and more consistent assertion. Included a code suggestion.

Positive observations:

  • The waitForInteractiveCounter hydration gate pattern is well-designed — callers correctly use relative offsets instead of assuming initialCount === 0.
  • The error recovery test nicely complements the existing hard-nav error test in error-interactive.spec.ts by covering the client-nav-away-and-back case.

github run

- Add disableViteErrorOverlay() before triggering error in recovery test
  to prevent Vite dev overlay intercepting pointer events on error-go-home link
- Change not.toBeVisible() to not.toBeAttached() for hard-nav slot assertions
  (stronger: slot content is absent from DOM entirely, not just hidden)
@james-elicx
Copy link
Copy Markdown
Collaborator

/bigbonk re-review for issues. any blockers?

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped test PR. The 8 E2E tests cover meaningful user-observable behaviors (layout persistence, template remount semantics, error recovery via client-nav, history navigation, parallel slot behavior). Fixture additions are minimal and purposeful. One minor suggestion below, otherwise LGTM.

.catch(() => {
// best effort
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: disableViteErrorOverlay is now copy-pasted in 3 spec files (error-interactive.spec.ts, form.spec.ts, and this one) with identical implementations. Consider extracting it into tests/e2e/helpers.ts alongside waitForAppRouterHydration. Not blocking — it's a pre-existing pattern — but this PR is a good opportunity to consolidate since it already imports from ../helpers.

Comment on lines +291 to +292
await expect(page.locator('[data-testid="analytics-default"]')).not.toBeAttached();
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with the prior review feedback here. On a hard load to /dashboard/settings, the server renders default.tsx for these slots — the page-specific slot content (team-slot, analytics-slot) should not be in the DOM at all. not.toBeAttached() is the stronger assertion and consistent with lines 274-275 above.

Suggested change
await expect(page.locator('[data-testid="analytics-default"]')).not.toBeAttached();
});
await expect(page.locator('[data-testid="team-slot"]')).not.toBeAttached();
await expect(page.locator('[data-testid="analytics-slot"]')).not.toBeAttached();

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Apr 10, 2026

Review submitted. Here's a summary:

Approved with two minor comments:

  1. disableViteErrorOverlay duplication — This helper is now copy-pasted identically in 3 spec files. Suggested extracting it to tests/e2e/helpers.ts since this PR already imports from there. Not blocking.

  2. Assertion strength on parallel slots hard-nav test (lines 291-292) — Echoed the prior review's suggestion to use not.toBeAttached() instead of not.toBeVisible(). On a hard load the server renders default.tsx, so the page-specific slot content shouldn't be in the DOM at all. not.toBeAttached() is the stronger assertion and consistent with lines 274-275 in the same file. Included a code suggestion.

The tests are well-structured, the fixture changes are minimal and correct (counter in layout not page, template at root level), and the waitForInteractiveCounter pattern handles hydration timing gracefully with relative offsets.

github run

@james-elicx james-elicx merged commit 8d7ee18 into cloudflare:main Apr 10, 2026
25 checks passed
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.

2 participants