Skip to content

Apply Brouter improvements (#12559)#12560

Open
msynk wants to merge 21 commits into
bitfoundation:developfrom
msynk:12559-brouter-improvements
Open

Apply Brouter improvements (#12559)#12560
msynk wants to merge 21 commits into
bitfoundation:developfrom
msynk:12559-brouter-improvements

Conversation

@msynk

@msynk msynk commented Jul 3, 2026

Copy link
Copy Markdown
Member

closes #12559

  1. Leave guards — per-route "unsaved changes" blocking that cancels before the URL ever moves.
  2. External-nav confirmation — browser beforeunload dialog, always-on or toggled at runtime.
  3. Awaitable navigationNavigateAsync tells you if it succeeded, was cancelled, redirected, not found, failed, or superseded.
  4. History entry state — attach a payload to a history entry and read it back across Back/Forward.
  5. Functional query updates — change one query param, keep the rest.
  6. Redirect-loop safety — guard redirects to the current target mean "continue," never a loop.
  7. Error boundaries — nearest ErrorContent renders failures in place, with retry.
  8. .NET 10 NotFound interopNavigationManager.NotFound() support and real HTTP 404s under static SSR.
  9. Revalidation — re-run the current page's loaders after a mutation, no navigation.
  10. SWR loader cacheStaleTime makes Back/Forward instant; stale data renders now, refreshes in background.
  11. Link preloading — hover/viewport/render triggers warm the destination's loaders before the click.
  12. Deferred data<BrouterAwait> streams slow data in after the page reveals.
  13. AOT-safe persistence — plug a source-generated JSON context into the prerender state bridge.
  14. Pathless group routes — shared guard/loader/layout for children without adding URL segments.
  15. Named outlets — one route fills multiple layout regions via BrouterView.
  16. Keep-alive routes — component state survives navigating away and back.
  17. Lazy route loading — load route assemblies on demand, matched within the same navigation.
  18. View transitions — animated page changes via the browser's View Transitions API.
  19. Bit.Brouter.Generators — compile-time-safe URL builders generated from your route declarations.

Plus: 4 pre-existing bugs fixed, 77 tests added (205 total), a demo for every feature, full README coverage.

Summary by CodeRabbit

  • New Features

    • Introduced the new <Broute> route component with improved parameter binding, custom constraints, named outlets, keep-alive support, and discovered route matching.
    • Added richer navigation capabilities, including relative links, history entry state, awaitable navigation outcomes, query updates, and scroll/focus handling.
    • Added loader features such as caching, revalidation, deferred content, prerender state restoration, and background preloading.
    • Added support for route error content, view transitions, and compile-time-safe route URL generation.
  • Bug Fixes

    • Improved route matching, guard execution, trailing-slash handling, and render invalidation behavior across navigations.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 72f9e6fc-3f1b-4927-b548-605474a709b9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR renames the route component to Broute, adds registry-aware constraint parsing and assembly-based discovery, refactors navigation into preventive and commit phases, adds scroll/focus effects, and updates demos, tests, docs, build wiring, and benchmarks.

Changes

Broute routing, constraints, discovery, and navigation

Layer / File(s) Summary
Broute types and template binding
src/Brouter/Bit.Brouter/Broute.cs, src/Brouter/Bit.Brouter/BrouterOutlet.cs, src/Brouter/Bit.Brouter/BrouterNavigationContext.cs, src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs, src/Brouter/Bit.Brouter/BrouterRouteData.cs
BrouterRoute is renamed to Broute, nested route wiring uses Broute types, template parameter names are cached, typed route data/meta wrappers are cascaded, and conventional parameter binding can be enabled by name.
Constraint registry and route discovery
src/Brouter/Bit.Brouter/BrouterConstraints.cs, src/Brouter/Bit.Brouter/BrouterConstraintRegistry.cs, src/Brouter/Bit.Brouter/BrouterRouteConstraint.cs, src/Brouter/Bit.Brouter/BrouterTemplateParser.cs, src/Brouter/Bit.Brouter/BrouterTemplateSegment.cs, src/Brouter/Bit.Brouter/BrouterOptions.cs, src/Brouter/Bit.Brouter/BrouterScrollPositionStorage.cs, src/Brouter/Bit.Brouter/BrouteScanner.cs, src/Brouter/Bit.Brouter/BroutePrerenderState.cs
Constraint registration moves to a per-container registry, template parsing threads the registry into constraint resolution, routable components can be discovered from assemblies, and loader results can be captured and restored across prerender boundaries.
Matching, navigation pipeline, and prerender persistence
src/Brouter/Bit.Brouter/Brouter.cs, src/Brouter/Bit.Brouter/BrouterNavigationType.cs, src/Brouter/Bit.Brouter/BrouterService.cs, src/Brouter/Bit.Brouter/BrouterRelativeUrl.cs, src/Brouter/Bit.Brouter/IBrouter.cs, src/Brouter/Bit.Brouter/BrouterLink.cs, src/Brouter/Bit.Brouter/Scripts/bit-brouter.ts
Route selection, preventive navigation handling, loader staging, prerender restoration, relative URL handling, navigation type classification, and scroll/focus effects are rewritten around Broute and the new navigation flow.
Build and packaging support
src/Brouter/Bit.Brouter/Bit.Brouter.csproj, src/Brouter/Bit.Brouter/package.json, src/Brouter/Bit.Brouter/tsconfig.json, .gitignore
The project adds TypeScript/JavaScript build wiring, static asset publishing, package metadata, and ignore rules for generated JavaScript.

Estimated code review effort: 5 (Critical) | ~120 minutes

Demo markup, docs, and tests

Layer / File(s) Summary
Demo app and README
src/Brouter/InteralDemos/Core/AppRouter.razor, src/Brouter/InteralDemos/Core/Pages/DiscoveredPage.razor, src/Brouter/InteralDemos/Core/Shared/Header.razor, src/Brouter/README.md
The demo router, discovered page, header link, and README examples are updated for Broute, discovery, navigation, scroll, and constraint behavior.
Route-host Razor components
src/Brouter/Tests/Bit.Brouter.Tests/GuardHost.razor, LinkHost.razor, LoaderHost.razor, NamedRouteHost.razor, OptionalParamHost.razor, QueryHost.razor, SimpleHomeHost.razor, SpecificityHost.razor, ConstraintHost.razor, DiscoveryOverrideHost.razor, MultiReplaceLinkHost.razor, NestedIndexHost.razor, RenderInvalidationHost.razor, SequentialLoadersHost.razor, SwapRouteHost.razor, TypedDataHost.razor, TypedValueReader.razor, ValueReader.razor, DiscoveryHost.razor, NavigationEffectsHost.razor, NavigationTypeHost.razor, PreventiveGuardHost.razor, RelativeLinkHost.razor, RelativeRedirectHost.razor, AmbiguousRoutesHost.razor
Existing test hosts are updated to Broute, and new host components are added for discovery, ambiguity, loader ordering, navigation effects, relative navigation, render invalidation, and typed-data scenarios.
Behavior and integration tests
src/Brouter/Tests/Bit.Brouter.Tests/BrouterTests.cs, AmbiguousRouteTests.cs, BrouterLinkTests.cs, ConstraintIntegrationTests.cs, ConstraintRegistryTests.cs, DiscoveryAndPersistenceTests.cs, GuardAndLoaderTests.cs, NavigationEffectsTests.cs, NavigationTypeTests.cs, RelativeNavigationTests.cs, RenderInvalidationTests.cs, ResolveUrlTests.cs, RouteDataCascadeTests.cs
Test suites cover trailing slash behavior, ambiguity rejection, constraints, discovery, navigation type, relative URLs, guard flow, render invalidation, loader ordering, URL resolution, scroll effects, and route data wrapping.

Estimated code review effort: 5 (Critical) | ~120 minutes

JavaScript build and benchmarks

Layer / File(s) Summary
JS build pipeline
.gitignore, src/Brouter/Bit.Brouter/Bit.Brouter.csproj, src/Brouter/Bit.Brouter/package.json, src/Brouter/Bit.Brouter/tsconfig.json
Adds TypeScript/JS build wiring, static asset publishing, package metadata, and ignore rules for generated JavaScript.
Route scalability benchmarks
src/Brouter/Bit.Brouter.slnx, src/Brouter/Tests/Bit.Brouter.Benchmarks/Bit.Brouter.Benchmarks.csproj, src/Brouter/Tests/Bit.Brouter.Benchmarks/BenchmarkComponents.cs, src/Brouter/Tests/Bit.Brouter.Benchmarks/Harness.cs, src/Brouter/Tests/Bit.Brouter.Benchmarks/Program.cs, src/Brouter/Tests/Bit.Brouter.Benchmarks/README.md
Adds the benchmark project, benchmark components, measurement harness, console runner, and benchmark documentation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant NavigationManager
  participant Brouter
  participant Broute
  participant BrouterService
  participant BitBrouterJS

  NavigationManager->>Brouter: OnLocationChanging(to)
  Brouter->>Broute: InvokeGuardsAsync(ctx)
  alt cancel or redirect
    Broute-->>Brouter: ctx.Cancel()/ctx.Redirect(url)
    Brouter->>Brouter: NavigateInternal(prevented target)
  else approve
    Broute-->>Brouter: guard completes
  end
  Brouter->>Brouter: ProcessNavigationAsync(decisionAlreadyMade, navType)
  Brouter->>BrouterService: ApplyNavigationEffectsAsync / SaveScrollPositionAsync
  BrouterService->>BitBrouterJS: applyNavigationEffects(...) / saveScrollPosition(...)
  BitBrouterJS-->>BrouterService: DOM effects applied
Loading

Poem

I hopped through routes with twitch and cheer,
Broute is here, and guards are clear.
Fragments scroll and focus too,
Relative paths know what to do.
A rabbit grin and carrot glow —
this routey world now nicely flows.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and matches the broad Brouter improvements in the PR.
Linked Issues check ✅ Passed The PR covers the linked Brouter improvements: Broute rename, discovery, prerender state, navigation UX, and performance changes.
Out of Scope Changes check ✅ Passed The changes stay focused on Brouter features, docs, demos, tests, and support code for the linked improvements.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@msynk

msynk commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

@msynk Sure, I'll take another look at the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/Brouter/Bit.Brouter/wwwroot/BitBrouter.js (1)

134-137: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider bounding scrollPositions growth.

Every distinct URL leaves a permanent entry, and persistScrollPositions writes the whole map into a single storage slot on each save. Over a long-lived session (or query-param-heavy URLs like search/pagination) this grows unbounded and can hit the Web Storage quota; the quota error is swallowed in persistScrollPositions, so persistence silently stops. A small cap (e.g. LRU eviction of oldest keys) keeps it bounded without changing behavior for typical apps.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter/wwwroot/BitBrouter.js` around lines 134 - 137, The
scroll position cache in `scrollPositions` is unbounded, so each new URL can
permanently add entries and eventually overflow Web Storage. Update the save
path around `scrollPositions.set(...)` and `persistScrollPositions()` to enforce
a small cap on stored keys, evicting the oldest entries first (LRU/oldest-first
is fine) before persisting. Keep the change localized to the scroll persistence
logic in `BitBrouter.js` so typical `currentScroll()` behavior remains
unchanged.
src/Brouter/Bit.Brouter/BrouterService.cs (1)

368-427: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Duplicated catch blocks across the four JS-interop call sites.

ApplyNavigationEffectsAsync, SaveScrollPositionAsync, DisposeAsync, and GoAsync all repeat the same four-exception swallow list. Consider extracting a small SafeJsCallAsync helper to centralize this pattern and reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter/BrouterService.cs` around lines 368 - 427, The JS
interop error handling is duplicated across ApplyNavigationEffectsAsync,
SaveScrollPositionAsync, DisposeAsync, and GoAsync. Extract the repeated
try/catch swallow pattern into a small shared helper such as SafeJsCallAsync in
BrouterService, and have each of those call sites wrap their module invocation
through it so the same four exception types are handled in one place.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Brouter/Bit.Brouter/Broute.cs`:
- Around line 122-125: Update the stale comment in Broute so it matches the
current constraint resolution flow: the parser in Broute.ParseTemplate should be
described as using the DI-container-scoped Brouter.Options.Constraints plus
built-in constraints only. Remove the reference to a “process-wide registry”
since the global static registry has been removed, and keep the comment aligned
with BrouterConstraintRegistry and BrouterTemplateParser.ParseTemplate.

In `@src/Brouter/Bit.Brouter/BroutePrerenderState.cs`:
- Around line 64-84: TryRestore currently only guards the JSON deserialization
path, but the Type.GetType lookup can still throw for stale or unloadable
persisted type names and should fall back to rerunning the loader instead of
crashing. Update BroutePrerenderState.TryRestore to catch type-resolution
failures around the Type.GetType(state.TypeName, throwOnError: false) step and
return false on those failures, using the same fallback behavior already used
for a missing type or JsonException.

In `@src/Brouter/Bit.Brouter/Brouter.cs`:
- Line 279: The `_navigating` flag in `Brouter` is never driven, so the
`Navigating` pending UI in `BuildRenderTree` never appears. Update
`ProcessNavigationAsync` (especially its loader loop) to set `_navigating` to
true when navigation starts and back to false when it completes or aborts, and
ensure state changes trigger a rerender so the `Navigating` fragment is
shown/hidden correctly. Keep the logic aligned with the existing `_navigating`
field and `Navigating` parameter behavior.

In `@src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs`:
- Around line 99-100: The ApplyTypedParameters call in BrouterRouteRenderer can
leave reused component instances with stale route-parameter values when a
parameter disappears from the current template. Update the parameter application
logic around ApplyTypedParameters so that route parameters not present in the
current _route.TemplateParameterNames are explicitly emitted as default/cleared
frames instead of being skipped, ensuring reused components do not retain values
from a previous route.

In `@src/Brouter/README.md`:
- Around line 254-256: Clarify the navigation precedence wording in the README
so it explicitly states that when a fragment target resolves, it scrolls and
takes focus and no further focus handling occurs. Update the precedence sentence
around the navigation behavior description to make it clear that
FocusOnNavigateSelector only applies when no fragment target was resolved, so
the rules do not imply both focus targets run on the same navigation.

---

Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterService.cs`:
- Around line 368-427: The JS interop error handling is duplicated across
ApplyNavigationEffectsAsync, SaveScrollPositionAsync, DisposeAsync, and GoAsync.
Extract the repeated try/catch swallow pattern into a small shared helper such
as SafeJsCallAsync in BrouterService, and have each of those call sites wrap
their module invocation through it so the same four exception types are handled
in one place.

In `@src/Brouter/Bit.Brouter/wwwroot/BitBrouter.js`:
- Around line 134-137: The scroll position cache in `scrollPositions` is
unbounded, so each new URL can permanently add entries and eventually overflow
Web Storage. Update the save path around `scrollPositions.set(...)` and
`persistScrollPositions()` to enforce a small cap on stored keys, evicting the
oldest entries first (LRU/oldest-first is fine) before persisting. Keep the
change localized to the scroll persistence logic in `BitBrouter.js` so typical
`currentScroll()` behavior remains unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: b9b04859-9f89-402b-bc03-dd74ed9d7c93

📥 Commits

Reviewing files that changed from the base of the PR and between 2bf38b5 and 70fd776.

📒 Files selected for processing (37)
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/BroutePrerenderState.cs
  • src/Brouter/Bit.Brouter/BrouteScanner.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterConstraintRegistry.cs
  • src/Brouter/Bit.Brouter/BrouterConstraints.cs
  • src/Brouter/Bit.Brouter/BrouterLink.cs
  • src/Brouter/Bit.Brouter/BrouterNavigationContext.cs
  • src/Brouter/Bit.Brouter/BrouterNavigationType.cs
  • src/Brouter/Bit.Brouter/BrouterOptions.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterRouteConstraint.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/BrouterScrollPositionStorage.cs
  • src/Brouter/Bit.Brouter/BrouterService.cs
  • src/Brouter/Bit.Brouter/BrouterTemplateParser.cs
  • src/Brouter/Bit.Brouter/BrouterTemplateSegment.cs
  • src/Brouter/Bit.Brouter/wwwroot/BitBrouter.js
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/Pages/DiscoveredPage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/BrouterLinkTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/BrouterTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ConstraintHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ConstraintIntegrationTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ConstraintRegistryTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/DiscoveredPage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/DiscoveryAndPersistenceTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/DiscoveryHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationTypeHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationTypeTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/PreventiveGuardHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/SampleDto.cs
💤 Files with no reviewable changes (1)
  • src/Brouter/Bit.Brouter/BrouterConstraints.cs
✅ Files skipped from review due to trivial changes (5)
  • src/Brouter/Tests/Bit.Brouter.Tests/DiscoveredPage.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/PreventiveGuardHost.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/Bit.Brouter/BrouterNavigationType.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/BrouterLinkTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Brouter/InteralDemos/Core/AppRouter.razor

Comment thread src/Brouter/Bit.Brouter/Broute.cs
Comment thread src/Brouter/Bit.Brouter/BroutePrerenderState.cs Outdated
Comment thread src/Brouter/Bit.Brouter/Brouter.cs
Comment thread src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs Outdated
Comment thread src/Brouter/README.md Outdated
@msynk

msynk commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

@msynk Sure, I'll take another look at the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs (1)

46-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test doesn't actually assert concurrent execution.

The test only asserts render completion within 3s; per the inline comment this is meant to prove overlap indirectly (a sequential implementation would time out). Consider directly asserting on an events/timestamp collection (similar to the sequential test above) to make the concurrency guarantee explicit and resistant to a slow-but-still-sequential regression that happens to finish under 3s.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs` around lines 46 -
58, The ParallelLoaders_runs_chain_loaders_concurrently test is only checking
that rendering finishes, which does not explicitly prove overlap. Update the
test in GuardAndLoaderTests to assert concurrent execution directly, using the
same event/timestamp collection approach as the sequential loader test and
referencing ParallelLoadersHost so you can verify both loaders started before
either completed. Keep the final render assertion if needed, but make the
concurrency guarantee explicit by checking the recorded ordering/timing rather
than relying on a timeout-based proxy.
src/Brouter/Bit.Brouter/BrouterService.cs (1)

115-131: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider logging swallowed JS interop failures.

SafeJsCallAsync silently swallows JSDisconnectedException, JSException, InvalidOperationException, and TaskCanceledException across all interop call sites (module import, scroll/focus effects, history.go, module dispose). This is reasonable for expected transient conditions (disconnected circuit, pre-render), but a genuine misconfiguration (e.g., wrong content path breaking the module import) would also be swallowed with zero diagnostics, making it hard to notice broken scroll/focus behavior in production.

♻️ Optional: add debug-level logging on swallowed exceptions
-    private static async ValueTask SafeJsCallAsync(Func<ValueTask> call)
+    private static async ValueTask SafeJsCallAsync(Func<ValueTask> call, ILogger? logger = null)
     {
         try { await call(); }
-        catch (JSDisconnectedException) { /* circuit disconnected mid-call */ }
-        catch (JSException) { /* JS interop failure (e.g. non-browser host) */ }
-        catch (InvalidOperationException) { /* JS interop unavailable during pre-render */ }
-        catch (TaskCanceledException) { /* component disposed mid-call */ }
+        catch (JSDisconnectedException ex) { logger?.LogDebug(ex, "Circuit disconnected mid-call."); }
+        catch (JSException ex) { logger?.LogDebug(ex, "JS interop failure."); }
+        catch (InvalidOperationException ex) { logger?.LogDebug(ex, "JS interop unavailable during pre-render."); }
+        catch (TaskCanceledException ex) { logger?.LogDebug(ex, "Component disposed mid-call."); }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter/BrouterService.cs` around lines 115 - 131,
SafeJsCallAsync currently swallows JS interop exceptions with no diagnostics,
which can hide real failures at call sites like GoAsync and the other interop
uses in BrouterService. Update SafeJsCallAsync to log swallowed exceptions at
debug level (or equivalent) with enough context before suppressing them, while
still preserving the existing non-fatal behavior for JSDisconnectedException,
JSException, InvalidOperationException, and TaskCanceledException. Keep the
centralized handling in SafeJsCallAsync so all interop paths get the same
logging behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Brouter/Bit.Brouter/Bit.Brouter.csproj`:
- Around line 84-92: The BuildJavaScript target is not configuration-aware
because its Inputs/Outputs only track the TypeScript files and
wwwroot/bit-brouter.js, so a Debug build can incorrectly satisfy up-to-date
checks for a later Release build. Update the target in Bit.Brouter.csproj so the
incremental build key includes $(Configuration) or separate the Release
minify/move path into configuration-specific outputs, ensuring BuildJavaScript
reruns when switching between Debug and Release. Use the existing
BuildJavaScript target, the esbuild minify step, and the Move action as the
symbols to adjust.

In `@src/Brouter/Bit.Brouter/BrouterRelativeUrl.cs`:
- Around line 18-24: `IsRelative` in `BrouterRelativeUrl` does not treat
dot-prefixed refs with a مباشرة appended query or hash as relative, so `Resolve`
can send them through base-relative navigation instead of preserving the
intended target. Update `IsRelative(string url)` to recognize forms like ".?..."
and "..?..."/".#..."/"..#..." as relative alongside the existing "."/"./" and
".."/"../" cases, keeping the logic aligned with `Resolve`’s
query/hash-preservation behavior.

---

Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterService.cs`:
- Around line 115-131: SafeJsCallAsync currently swallows JS interop exceptions
with no diagnostics, which can hide real failures at call sites like GoAsync and
the other interop uses in BrouterService. Update SafeJsCallAsync to log
swallowed exceptions at debug level (or equivalent) with enough context before
suppressing them, while still preserving the existing non-fatal behavior for
JSDisconnectedException, JSException, InvalidOperationException, and
TaskCanceledException. Keep the centralized handling in SafeJsCallAsync so all
interop paths get the same logging behavior.

In `@src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs`:
- Around line 46-58: The ParallelLoaders_runs_chain_loaders_concurrently test is
only checking that rendering finishes, which does not explicitly prove overlap.
Update the test in GuardAndLoaderTests to assert concurrent execution directly,
using the same event/timestamp collection approach as the sequential loader test
and referencing ParallelLoadersHost so you can verify both loaders started
before either completed. Keep the final render assertion if needed, but make the
concurrency guarantee explicit by checking the recorded ordering/timing rather
than relying on a timeout-based proxy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: afea28b0-16f9-4a7e-b0b2-7c84c19ab68f

📥 Commits

Reviewing files that changed from the base of the PR and between 70fd776 and c484772.

⛔ Files ignored due to path filters (1)
  • src/Brouter/Bit.Brouter/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (43)
  • .gitignore
  • src/Brouter/Bit.Brouter.slnx
  • src/Brouter/Bit.Brouter/Bit.Brouter.csproj
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/BroutePrerenderState.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterLink.cs
  • src/Brouter/Bit.Brouter/BrouterNavigationContext.cs
  • src/Brouter/Bit.Brouter/BrouterRelativeUrl.cs
  • src/Brouter/Bit.Brouter/BrouterRouteData.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/BrouterService.cs
  • src/Brouter/Bit.Brouter/IBrouter.cs
  • src/Brouter/Bit.Brouter/Scripts/bit-brouter.ts
  • src/Brouter/Bit.Brouter/package.json
  • src/Brouter/Bit.Brouter/tsconfig.json
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Benchmarks/BenchmarkComponents.cs
  • src/Brouter/Tests/Bit.Brouter.Benchmarks/Bit.Brouter.Benchmarks.csproj
  • src/Brouter/Tests/Bit.Brouter.Benchmarks/Harness.cs
  • src/Brouter/Tests/Bit.Brouter.Benchmarks/Program.cs
  • src/Brouter/Tests/Bit.Brouter.Benchmarks/README.md
  • src/Brouter/Tests/Bit.Brouter.Tests/AmbiguousRouteTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/AmbiguousRoutesHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/BrouterLinkTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/DiscoveryOverrideHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/GuardAndLoaderTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/MultiReplaceLinkHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NestedIndexHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ParallelLoadersHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RelativeLinkHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RelativeNavigationTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/RelativeRedirectHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RenderInvalidationHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RenderInvalidationTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ResolveUrlTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/RouteDataCascadeTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/SequentialLoadersHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/SwapRouteHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/TypedDataHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/TypedValueReader.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ValueReader.razor
✅ Files skipped from review due to trivial changes (3)
  • src/Brouter/Bit.Brouter/tsconfig.json
  • src/Brouter/Bit.Brouter.slnx
  • src/Brouter/Bit.Brouter/IBrouter.cs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/Brouter/Bit.Brouter/BroutePrerenderState.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigationEffectsTests.cs
  • src/Brouter/Bit.Brouter/Broute.cs

Comment thread src/Brouter/Bit.Brouter/Bit.Brouter.csproj Outdated
Comment thread src/Brouter/Bit.Brouter/BrouterRelativeUrl.cs
@msynk

msynk commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

@msynk Sure, I'll take another look at the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@msynk

msynk commented Jul 4, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

@msynk Sure, I'll take another look at the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@msynk

msynk commented Jul 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

@msynk Sure, I'll take another look at the changes now.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
src/Brouter/Bit.Brouter/BrouterView.cs (1)

41-59: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Stale registration if the cascading Route instance itself changes.

The unregister/re-register logic only handles a changed Name, not a changed Route (e.g., if the component is reparented to a different Broute instance across renders, which can happen with dynamic outlet content). In that case the old Route keeps the stale fragment registered under _registeredName since only the new Route receives SetNamedView. This is an edge case unlikely to occur in typical usage where BrouterView is statically nested inside a fixed Broute.

Optional defensive fix
     protected override void OnParametersSet()
     {
         if (Route is null)
             throw new InvalidOperationException(
                 "A BrouterView must be declared inside a Broute (its ChildContent), whose parent layout hosts the named outlet.");

         if (string.IsNullOrEmpty(Name))
             throw new InvalidOperationException(
                 "BrouterView requires a non-empty Name. The route's main content already renders in the primary (unnamed) outlet via Content/Component.");

-        // Re-register every parameter pass: a host re-render produces a fresh fragment instance
-        // that the outlet must pick up, and a changed Name must vacate the old slot.
-        if (_registeredName is not null && string.Equals(_registeredName, Name, StringComparison.Ordinal) is false)
-        {
-            Route.SetNamedView(_registeredName, null);
-        }
+        // Re-register every parameter pass: a host re-render produces a fresh fragment instance
+        // that the outlet must pick up, and a changed Name or Route must vacate the old slot.
+        if (_registeredName is not null && (_registeredRoute != Route || string.Equals(_registeredName, Name, StringComparison.Ordinal) is false))
+        {
+            _registeredRoute?.SetNamedView(_registeredName, null);
+        }
         Route.SetNamedView(Name, ChildContent);
         _registeredName = Name;
+        _registeredRoute = Route;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter/BrouterView.cs` around lines 41 - 59, The
OnParametersSet logic in BrouterView only clears the previous registration when
Name changes, so a new cascading Route instance can leave the old Route with a
stale named view. Update the re-registration flow to detect when Route itself
changes, unregister the previously stored _registeredName from the old Route
before switching, then register ChildContent on the current Route and refresh
_registeredName. Keep the fix localized to BrouterView.OnParametersSet and the
_registeredName bookkeeping.
src/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cs (1)

55-83: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assertions run before the fire-and-forget invoke completes.

Both Revalidate_with_no_matched_chain_is_a_noop (Lines 56-67) and Routes_without_loaders_revalidate_as_a_noop (Lines 69-83) call cut.InvokeAsync(() => brouter.RevalidateAsync().AsTask()) without awaiting the returned task or wrapping the assertion in cut.WaitForAssertion, unlike every other test in this class. If RevalidateAsync throws or behaves asynchronously, these tests can pass trivially regardless of actual behavior, and any exception would be silently swallowed as an unobserved task fault.

🧪 Proposed fix
-        var brouter = Services.GetRequiredService<IBrouter>();
-        cut.InvokeAsync(() => brouter.RevalidateAsync().AsTask());
-
-        Assert.AreEqual(0, cut.Instance.ParentLoaderRuns);
-        Assert.AreEqual(0, cut.Instance.ChildLoaderRuns);
+        var brouter = Services.GetRequiredService<IBrouter>();
+        await cut.InvokeAsync(() => brouter.RevalidateAsync().AsTask());
+
+        Assert.AreEqual(0, cut.Instance.ParentLoaderRuns);
+        Assert.AreEqual(0, cut.Instance.ChildLoaderRuns);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cs` around lines 55 - 83,
The two no-op revalidation tests are asserting before `IBrouter.RevalidateAsync`
finishes because `cut.InvokeAsync(...)` is fire-and-forget. Update
`Revalidate_with_no_matched_chain_is_a_noop` and
`Routes_without_loaders_revalidate_as_a_noop` to await the `InvokeAsync` task or
verify results via `cut.WaitForAssertion`, matching the async pattern used
elsewhere in `RevalidateTests`. Ensure the assertions on `ParentLoaderRuns`,
`ChildLoaderRuns`, and the rendered `other` element happen only after
`RevalidateAsync` completes so failures are observed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs`:
- Around line 51-69: The route generator currently deduplicates templates in
Emit but still passes wildcard routes through, and EmitMethod does not handle
SegmentKind.Wildcard. Update BrouterRoutesGenerator so wildcard templates
emitted by RouteTemplateParser are either skipped during Emit or surfaced as a
diagnostic before generation, and make sure EmitMethod/related switch logic has
an explicit path for SegmentKind.Wildcard to avoid producing incomplete URL
builders.
- Around line 34-69: Duplicate route templates are still resolved by whichever
entry is seen first in BrouterRoutesGenerator.Emit, which silently drops
conflicting explicit names. Update the dedup logic in Emit to detect when the
same Template appears with conflicting route metadata (especially different
non-null Name values) and report a diagnostic through the generator context
instead of choosing a winner arbitrarily; keep the existing stable sort/output
path unchanged.

In `@src/Brouter/Bit.Brouter/BrouterAwait.cs`:
- Around line 41-68: The async render path in BrouterAwait’s ObserveAsync can
resume after the component has started disposing, so guard the final
InvokeAsync(StateHasChanged) call with a disposal check. Add a _disposed flag or
cancellation token to BrouterAwait, set it during disposal, and ensure
ObserveAsync exits without scheduling a render once disposal has begun; keep the
existing ReferenceEquals(_observed, task) check as part of the guard.

In `@src/Brouter/README.md`:
- Around line 320-342: Clarify the caching behavior in the README section around
preloading and loader caching: “No StaleTime anywhere → no caching” applies only
to the normal loader cache, while preloaded results are cached separately and
may still be reused during navigation via PreloadAsync and BrouterLinkPreload
with PreloadStaleTime. Update the wording in the StaleTime/preloading
explanation to distinguish the loader cache from the preload cache and avoid
implying that all caching is disabled when StaleTime is absent.

In
`@src/Brouter/Tests/Bit.Brouter.Generators.Tests/Bit.Brouter.Generators.Tests.csproj`:
- Around line 1-27: The test project is missing the shared build import/target
setup, so CI cannot resolve InstallNodejsDependencies. Update
Bit.Brouter.Generators.Tests.csproj to match the generator project’s build
integration by importing the shared Bit.Build.props/Bit.Build.targets pattern
and ensuring the InstallNodejsDependencies target is available. Use the existing
Bit.Brouter.Generators.csproj and its build-related symbols as the reference for
where the common build files and target wiring should be added.

In `@src/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncTests.cs`:
- Around line 20-129: The test methods in NavigateAsyncTests are reading the
navigation ValueTask before the cut.InvokeAsync dispatch has completed, so
navigation can still be the default value and produce a bogus outcome. Update
the affected tests to be async Task methods, await cut.InvokeAsync when
assigning navigation from BrouterNavigation.NavigateAsync, and then call Await
only after the dispatch has finished so the real ValueTask is observed.

---

Nitpick comments:
In `@src/Brouter/Bit.Brouter/BrouterView.cs`:
- Around line 41-59: The OnParametersSet logic in BrouterView only clears the
previous registration when Name changes, so a new cascading Route instance can
leave the old Route with a stale named view. Update the re-registration flow to
detect when Route itself changes, unregister the previously stored
_registeredName from the old Route before switching, then register ChildContent
on the current Route and refresh _registeredName. Keep the fix localized to
BrouterView.OnParametersSet and the _registeredName bookkeeping.

In `@src/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cs`:
- Around line 55-83: The two no-op revalidation tests are asserting before
`IBrouter.RevalidateAsync` finishes because `cut.InvokeAsync(...)` is
fire-and-forget. Update `Revalidate_with_no_matched_chain_is_a_noop` and
`Routes_without_loaders_revalidate_as_a_noop` to await the `InvokeAsync` task or
verify results via `cut.WaitForAssertion`, matching the async pattern used
elsewhere in `RevalidateTests`. Ensure the assertions on `ParentLoaderRuns`,
`ChildLoaderRuns`, and the rendered `other` element happen only after
`RevalidateAsync` completes so failures are observed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 931608d6-38f7-480f-9527-d182d4ca6dd0

📥 Commits

Reviewing files that changed from the base of the PR and between 7a28e64 and 448ed58.

📒 Files selected for processing (75)
  • src/Brouter/Bit.Brouter.Generators/Bit.Brouter.Generators.csproj
  • src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs
  • src/Brouter/Bit.Brouter.Generators/IsExternalInit.cs
  • src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs
  • src/Brouter/Bit.Brouter.Generators/RouteModel.cs
  • src/Brouter/Bit.Brouter.Generators/RouteTemplateParser.cs
  • src/Brouter/Bit.Brouter.slnx
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/BroutePrerenderState.cs
  • src/Brouter/Bit.Brouter/Brouter.cs
  • src/Brouter/Bit.Brouter/BrouterAwait.cs
  • src/Brouter/Bit.Brouter/BrouterErrorContext.cs
  • src/Brouter/Bit.Brouter/BrouterLink.cs
  • src/Brouter/Bit.Brouter/BrouterLinkPreload.cs
  • src/Brouter/Bit.Brouter/BrouterLoaderCache.cs
  • src/Brouter/Bit.Brouter/BrouterLocation.cs
  • src/Brouter/Bit.Brouter/BrouterNavigationContext.cs
  • src/Brouter/Bit.Brouter/BrouterNavigationOutcome.cs
  • src/Brouter/Bit.Brouter/BrouterOptions.cs
  • src/Brouter/Bit.Brouter/BrouterOutlet.cs
  • src/Brouter/Bit.Brouter/BrouterQueryBuilder.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/BrouterService.cs
  • src/Brouter/Bit.Brouter/BrouterStaleReloadMode.cs
  • src/Brouter/Bit.Brouter/BrouterView.cs
  • src/Brouter/Bit.Brouter/IBrouter.cs
  • src/Brouter/Bit.Brouter/Scripts/bit-brouter.ts
  • src/Brouter/InteralDemos/Core/AppRouter.razor
  • src/Brouter/InteralDemos/Core/Bit.Brouter.Demos.Core.csproj
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/InteralDemos/Core/Extensions/IServiceCollectionExtensions.cs
  • src/Brouter/InteralDemos/Core/Pages/DataPage.razor
  • src/Brouter/InteralDemos/Core/Pages/DeferredPage.razor
  • src/Brouter/InteralDemos/Core/Pages/EditorPage.razor
  • src/Brouter/InteralDemos/Core/Pages/HistoryStatePage.razor
  • src/Brouter/InteralDemos/Core/Pages/HomePage.razor
  • src/Brouter/InteralDemos/Core/Pages/OutcomesPage.razor
  • src/Brouter/InteralDemos/Core/Pages/StickyNotePage.razor
  • src/Brouter/InteralDemos/Core/Pages/UnstablePage.razor
  • src/Brouter/InteralDemos/Core/Shared/Header.razor
  • src/Brouter/README.md
  • src/Brouter/Tests/Bit.Brouter.Generators.Tests/Bit.Brouter.Generators.Tests.csproj
  • src/Brouter/Tests/Bit.Brouter.Generators.Tests/BrouterRoutesGeneratorTests.cs
  • src/Brouter/Tests/Bit.Brouter.Generators.Tests/GeneratorTestHarness.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/CacheHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/CachedView.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ChildDataView.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/DeferredDataTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/DeferredHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/DiscoveryAndPersistenceTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ErrorContentTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/GroupRouteHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/GroupRouteTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/HistoryStateHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/HistoryStateTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LeaveGuardHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/LeaveGuardTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/LoaderCacheTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NamedOutletHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NamedOutletTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/NotFoundInteropHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/NotFoundInteropTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/OnNavigateAsyncTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/OnNavigateHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/QueryBuilderTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/RevalidateHost.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/RevalidateTests.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/SampleDto.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/StatefulCounter.razor
  • src/Brouter/Tests/Bit.Brouter.Tests/ViewTransitionTests.cs
✅ Files skipped from review due to trivial changes (4)
  • src/Brouter/Bit.Brouter/BrouterLinkPreload.cs
  • src/Brouter/Tests/Bit.Brouter.Tests/KeepAliveHost.razor
  • src/Brouter/InteralDemos/Core/DemoState.cs
  • src/Brouter/Bit.Brouter.slnx
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/Brouter/Bit.Brouter/BroutePrerenderState.cs
  • src/Brouter/Bit.Brouter/BrouterNavigationContext.cs
  • src/Brouter/Bit.Brouter/Broute.cs
  • src/Brouter/Bit.Brouter/BrouterLink.cs
  • src/Brouter/Bit.Brouter/BrouterRouteRenderer.cs
  • src/Brouter/Bit.Brouter/BrouterService.cs

Comment on lines +34 to +69
var rootNamespace = context.AnalyzerConfigOptionsProvider
.Select(static (provider, _) =>
provider.GlobalOptions.TryGetValue("build_property.RootNamespace", out var ns) && string.IsNullOrWhiteSpace(ns) is false
? ns
: "Bit.Brouter.Generated");

context.RegisterSourceOutput(routesPerFile.Combine(rootNamespace), static (spc, input) =>
{
var (perFile, ns) = input;
var source = Emit(perFile, ns);
if (source is not null)
{
spc.AddSource("BrouterRoutes.g.cs", SourceText.From(source, Encoding.UTF8));
}
});
}

private static string? Emit(ImmutableArray<EquatableArray<RouteModel>> perFile, string ns)
{
// Dedup by canonical template (case-insensitive; parameter names don't distinguish
// templates at runtime, but for generation the first declaration's names/types win -
// matching the router's "hand-declared shadows discovered" precedence closely enough).
// A named declaration is preferred over an unnamed duplicate so Names constants survive.
var byTemplate = new Dictionary<string, RouteModel>(StringComparer.OrdinalIgnoreCase);
foreach (var file in perFile)
{
foreach (var route in file.Items)
{
if (byTemplate.TryGetValue(route.Template, out var existing))
{
if (existing.Name is null && route.Name is not null) byTemplate[route.Template] = route;
continue;
}
byTemplate[route.Template] = route;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Repo root: '; pwd
printf '\nRelevant files:\n'
git ls-files | rg 'src/Brouter/Bit\.Brouter\.Generators/|RazorRouteScanner|RouteModel|BrouterRoutesGenerator'
printf '\nOutline generator file:\n'
ast-grep outline src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs --view expanded || true
printf '\nOutline scanner/model files if present:\n'
for f in $(git ls-files | rg 'RazorRouteScanner|RouteModel'); do
  echo "== $f =="
  ast-grep outline "$f" --view expanded || true
done

Repository: bitfoundation/bitplatform

Length of output: 1021


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the generator and nearby types in small slices with line numbers.
for f in src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs; do
  echo "==== $f (1-260) ===="
  nl -ba "$f" | sed -n '1,260p'
done

Repository: bitfoundation/bitplatform

Length of output: 282


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any diagnostics or conflict handling around route names/templates.
rg -n "ReportDiagnostic|duplicate|conflict|Name is not null|Template" src/Brouter/Bit.Brouter.Generators src/Brouter -g '!**/bin/**' -g '!**/obj/**'

Repository: bitfoundation/bitplatform

Length of output: 20889


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe whether the code itself imposes any ordering before the Collect() aggregation.
rg -n "AdditionalTextsProvider|Collect\(\)|OrderBy|ThenBy|Select\(static \(text" src/Brouter/Bit.Brouter.Generators -g '!**/bin/**' -g '!**/obj/**'

Repository: bitfoundation/bitplatform

Length of output: 642


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs

Repository: bitfoundation/bitplatform

Length of output: 12224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== RouteModel.cs =='
sed -n '1,220p' src/Brouter/Bit.Brouter.Generators/RouteModel.cs
printf '\n%s\n' '== RazorRouteScanner.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs
printf '\n%s\n' '== BrouterRoutesGeneratorTests.cs =='
sed -n '1,260p' src/Brouter/Tests/Bit.Brouter.Generators.Tests/BrouterRoutesGeneratorTests.cs

Repository: bitfoundation/bitplatform

Length of output: 14782


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "MakeUnique|methodNames|Names constants|explicit name|RouteModel\(|OrderBy\(r => r.Template|dedup" src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs
sed -n '80,220p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs

Repository: bitfoundation/bitplatform

Length of output: 7893


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "duplicate|ambiguous|name" src/Brouter/Tests/Bit.Brouter.Generators.Tests src/Brouter/Tests -g '!**/bin/**' -g '!**/obj/**'

Repository: bitfoundation/bitplatform

Length of output: 16678


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== BrouterRoutesGenerator.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs

printf '\n%s\n' '== RouteModel.cs =='
sed -n '1,220p' src/Brouter/Bit.Brouter.Generators/RouteModel.cs

printf '\n%s\n' '== RazorRouteScanner.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs

Repository: bitfoundation/bitplatform

Length of output: 20601


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== BrouterRoutesGenerator.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs

printf '\n%s\n' '== RouteModel.cs =='
sed -n '1,220p' src/Brouter/Bit.Brouter.Generators/RouteModel.cs

printf '\n%s\n' '== RazorRouteScanner.cs =='
sed -n '1,260p' src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs

printf '\n%s\n' '== BrouterRoutesGeneratorTests.cs =='
sed -n '1,260p' src/Brouter/Tests/Bit.Brouter.Generators.Tests/BrouterRoutesGeneratorTests.cs

Repository: bitfoundation/bitplatform

Length of output: 26876


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== generator name/dedup logic =='
rg -n "MakeUnique|methodNames|byTemplate|OrderBy\(r => r.Template|Names constants|ReportDiagnostic" src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs

printf '\n%s\n' '== tests mentioning duplicates/conflicts =='
rg -n "duplicate|ambiguous|conflict|name" src/Brouter/Tests/Bit.Brouter.Generators.Tests src/Brouter/Tests -g '!**/bin/**' -g '!**/obj/**'

Repository: bitfoundation/bitplatform

Length of output: 17841


Handle duplicate templates deterministically
src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs:56-67 The routes are already sorted after dedup, so output order is stable; the remaining issue is that duplicate templates still pick a winner by file enumeration, and conflicting explicit Names are silently dropped. Report a diagnostic instead of keeping one arbitrarily.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs` around lines 34
- 69, Duplicate route templates are still resolved by whichever entry is seen
first in BrouterRoutesGenerator.Emit, which silently drops conflicting explicit
names. Update the dedup logic in Emit to detect when the same Template appears
with conflicting route metadata (especially different non-null Name values) and
report a diagnostic through the generator context instead of choosing a winner
arbitrarily; keep the existing stable sort/output path unchanged.

Comment on lines +51 to +69
private static string? Emit(ImmutableArray<EquatableArray<RouteModel>> perFile, string ns)
{
// Dedup by canonical template (case-insensitive; parameter names don't distinguish
// templates at runtime, but for generation the first declaration's names/types win -
// matching the router's "hand-declared shadows discovered" precedence closely enough).
// A named declaration is preferred over an unnamed duplicate so Names constants survive.
var byTemplate = new Dictionary<string, RouteModel>(StringComparer.OrdinalIgnoreCase);
foreach (var file in perFile)
{
foreach (var route in file.Items)
{
if (byTemplate.TryGetValue(route.Template, out var existing))
{
if (existing.Name is null && route.Name is not null) byTemplate[route.Template] = route;
continue;
}
byTemplate[route.Template] = route;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how Wildcard segments are produced/consumed across the generator project.
fd -e cs . src/Brouter/Bit.Brouter.Generators -x rg -n -A3 -B3 'Wildcard' {}

Repository: bitfoundation/bitplatform

Length of output: 594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the generator and scanner/parser files, then inspect the relevant slices.
for f in \
  src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs \
  src/Brouter/Bit.Brouter.Generators/RouteModel.cs \
  src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs \
  src/Brouter/Bit.Brouter.Generators/RouteTemplateParser.cs
do
  echo "===== OUTLINE: $f ====="
  ast-grep outline "$f" --view expanded || true
  echo
done

echo "===== GENERATOR WILDCARD / SWITCH REFERENCES ====="
rg -n -A6 -B6 'SegmentKind\.Wildcard|switch .*SegmentKind|case SegmentKind\.' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs src/Brouter/Bit.Brouter.Generators/RouteModel.cs src/Brouter/Bit.Brouter.Generators/RazorRouteScanner.cs src/Brouter/Bit.Brouter.Generators/RouteTemplateParser.cs || true

echo "===== COLLECT / ORDER / DEDUP REFERENCES ====="
rg -n -A4 -B4 'Collect\(|OrderBy\(|ImmutableArray<EquatableArray<RouteModel>>|AdditionalTextsProvider|byTemplate|first declaration wins' src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs || true

Repository: bitfoundation/bitplatform

Length of output: 8097


Wildcard templates should be skipped or diagnosed
RouteTemplateParser emits SegmentKind.Wildcard, but EmitMethod has no case for it. */** routes will generate incomplete URL builders instead of being excluded or reported.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter.Generators/BrouterRoutesGenerator.cs` around lines 51
- 69, The route generator currently deduplicates templates in Emit but still
passes wildcard routes through, and EmitMethod does not handle
SegmentKind.Wildcard. Update BrouterRoutesGenerator so wildcard templates
emitted by RouteTemplateParser are either skipped during Emit or surfaced as a
diagnostic before generation, and make sure EmitMethod/related switch logic has
an explicit path for SegmentKind.Wildcard to avoid producing incomplete URL
builders.

Comment on lines +41 to +68
protected override void OnParametersSet()
{
if (ReferenceEquals(_observed, Task)) return;

_observed = Task;
if (Task is { IsCompleted: false })
{
_ = ObserveAsync(Task);
}
}

private async Task ObserveAsync(Task<TValue> task)
{
try
{
await task;
}
catch
{
// The failure renders via the task's own status below; awaiting here only observes the
// exception so it never surfaces as an unobserved-task crash.
}

if (ReferenceEquals(_observed, task))
{
await InvokeAsync(StateHasChanged);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target file with line numbers.
sed -n '1,220p' src/Brouter/Bit.Brouter/BrouterAwait.cs | cat -n

printf '\n---\n'

# Find disposal-related members in the component and nearby references.
rg -n --hidden -S "class BrouterAwait|IDisposable|IAsyncDisposable|Dispose\(|StateHasChanged|InvokeAsync\(" src/Brouter/Bit.Brouter

printf '\n---\n'

# Look for similar async-observation patterns elsewhere in the repo.
rg -n --hidden -S "ObserveAsync|await InvokeAsync\(StateHasChanged\)|_disposed|CancellationTokenSource" src

Repository: bitfoundation/bitplatform

Length of output: 40799


Guard the async render callback against disposal. ObserveAsync can still resume after the component is removed, so InvokeAsync(StateHasChanged) should be skipped once disposal begins. Add a _disposed flag or cancellation token and check it before scheduling the render.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Bit.Brouter/BrouterAwait.cs` around lines 41 - 68, The async
render path in BrouterAwait’s ObserveAsync can resume after the component has
started disposing, so guard the final InvokeAsync(StateHasChanged) call with a
disposal check. Add a _disposed flag or cancellation token to BrouterAwait, set
it during disposal, and ensure ObserveAsync exits without scheduling a render
once disposal has begun; keep the existing ReferenceEquals(_observed, task)
check as part of the guard.

Comment thread src/Brouter/README.md
Comment on lines +320 to +342
With a `StaleTime` (per-route, or `o.DefaultLoaderStaleTime` globally), loader results cache per
URL (path + query):

- **fresh** hit (younger than `StaleTime`) → the loader is skipped entirely - Back/Forward becomes instant;
- **stale** hit → by default (`o.StaleReloadMode = Background`) the cached data renders immediately
and a background revalidation refreshes it (classic SWR); `Blocking` treats stale as a miss;
- entries die after `o.LoaderCacheGcTime` (30 min default) and the store is capped at
`o.MaxLoaderCacheEntries` (50), oldest evicted first.

No `StaleTime` anywhere → no caching, exactly the previous behavior.

### Preloading

```razor
<BrouterLink Href="/users/42" Preload="BrouterLinkPreload.Intent">Saleh</BrouterLink>
```

`Intent` runs the destination's loaders into the cache on hover/touch/focus (debounced by
`o.PreloadDelay`, 50 ms); `Viewport` fires once when the link scrolls into view; `Render` fires
immediately; `o.DefaultLinkPreload` sets an app-wide default. Programmatic:
`await brouter.PreloadAsync("/users/42")`. Preloads run **loaders only** - no guards, no rendering -
and a preloaded result younger than `o.PreloadStaleTime` (30 s) is used by the real navigation even
on routes with no `StaleTime`. Keep preloaded loaders side-effect-free (`ctx.IsPreload` is set).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant README section with line numbers
sed -n '300,360p' src/Brouter/README.md | cat -n

# Find the cache/preload terminology in the repo
rg -n --hidden --no-ignore-vcs "PreloadStaleTime|DefaultLoaderStaleTime|StaleReloadMode|LoaderCacheGcTime|MaxLoaderCacheEntries|PreloadDelay|DefaultLinkPreload|PreloadAsync|StaleTime" src/Brouter

Repository: bitfoundation/bitplatform

Length of output: 15536


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the preload/cache implementation that the README describes
sed -n '2038,2058p' src/Brouter/Bit.Brouter/Brouter.cs | cat -n
printf '\n---\n'
sed -n '1,90p' src/Brouter/Bit.Brouter/BrouterLoaderCache.cs | cat -n

Repository: bitfoundation/bitplatform

Length of output: 5851


Clarify preload vs normal loader caching
src/Brouter/README.md:329 — No StaleTime anywhere → no caching is only true for the normal loader cache; preloaded results are still cached separately and can be reused on navigation for PreloadStaleTime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/README.md` around lines 320 - 342, Clarify the caching behavior
in the README section around preloading and loader caching: “No StaleTime
anywhere → no caching” applies only to the normal loader cache, while preloaded
results are cached separately and may still be reused during navigation via
PreloadAsync and BrouterLinkPreload with PreloadStaleTime. Update the wording in
the StaleTime/preloading explanation to distinguish the loader cache from the
preload cache and avoid implying that all caching is disabled when StaleTime is
absent.

Comment on lines +1 to +27
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<NoWarn>$(NoWarn);CS1591</NoWarn>
<EnableMSTestRunner>true</EnableMSTestRunner>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\..\..\AssemblyOriginatorKeyFile.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MSTest" Version="4.2.3" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0" />
</ItemGroup>

<ItemGroup>
<!-- Plain assembly reference: tests instantiate the generator directly and drive it with
CSharpGeneratorDriver rather than consuming it as an analyzer. -->
<ProjectReference Include="..\..\Bit.Brouter.Generators\Bit.Brouter.Generators.csproj" />
</ItemGroup>

</Project>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Same CI-breaking MSB4057 failure as Bit.Brouter.Generators.csproj.

This test project also lacks the common build import and target that CI expects (InstallNodejsDependencies), and it doesn't even import a shared Bit.Build.props/Bit.Build.targets file like its sibling generator project does. See the detailed comment on Bit.Brouter.Generators.csproj for the root-cause verification script; the same fix should cover both projects.

🧰 Tools
🪛 GitHub Actions: bit platform CI - Brouter / 0_build Bit.Brouter.txt

[error] 1-1: MSBuild failed (MSB4057): The target "InstallNodejsDependencies" does not exist in the project.

🪛 GitHub Actions: bit platform CI - Brouter / build Bit.Brouter

[error] 1-1: MSB4057: The target "InstallNodejsDependencies" does not exist in the project.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@src/Brouter/Tests/Bit.Brouter.Generators.Tests/Bit.Brouter.Generators.Tests.csproj`
around lines 1 - 27, The test project is missing the shared build import/target
setup, so CI cannot resolve InstallNodejsDependencies. Update
Bit.Brouter.Generators.Tests.csproj to match the generator project’s build
integration by importing the shared Bit.Build.props/Bit.Build.targets pattern
and ensuring the InstallNodejsDependencies target is available. Use the existing
Bit.Brouter.Generators.csproj and its build-related symbols as the reference for
where the common build files and target wiring should be added.

Source: Pipeline failures

Comment on lines +20 to +129
private BrouterNavigationOutcome Await(IRenderedComponent<NavigateAsyncHost> cut, ValueTask<BrouterNavigationOutcome> navigation)
{
var task = navigation.AsTask();
cut.WaitForAssertion(() => Assert.IsTrue(task.IsCompleted, "navigation outcome did not resolve"));
return task.GetAwaiter().GetResult();
}

[TestMethod]
public void Successful_navigation_resolves_Succeeded()
{
var (cut, brouter) = RenderAtA();

ValueTask<BrouterNavigationOutcome> navigation = default;
cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/b"); });
var outcome = Await(cut, navigation);

Assert.AreEqual(BrouterNavigationStatus.Succeeded, outcome.Status);
Assert.IsTrue(outcome.Succeeded);
Assert.IsNotNull(cut.Find("[data-testid=b]"));
}

[TestMethod]
public void Guard_cancel_resolves_Cancelled_and_url_stays()
{
var (cut, brouter) = RenderAtA();
var nav = Services.GetRequiredService<FakeNavigationManager>();

ValueTask<BrouterNavigationOutcome> navigation = default;
cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/blocked"); });
var outcome = Await(cut, navigation);

Assert.AreEqual(BrouterNavigationStatus.Cancelled, outcome.Status);
Assert.IsTrue(nav.Uri.EndsWith("/a", StringComparison.Ordinal));
}

[TestMethod]
public void Guard_redirect_resolves_Redirected_with_the_target()
{
var (cut, brouter) = RenderAtA();

ValueTask<BrouterNavigationOutcome> navigation = default;
cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/redirect"); });
var outcome = Await(cut, navigation);

Assert.AreEqual(BrouterNavigationStatus.Redirected, outcome.Status);
Assert.AreEqual("/b", outcome.RedirectedTo);
cut.WaitForAssertion(() => cut.Find("[data-testid=b]"));
}

[TestMethod]
public void RedirectTo_route_resolves_Redirected()
{
var (cut, brouter) = RenderAtA();

ValueTask<BrouterNavigationOutcome> navigation = default;
cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/route-redirect"); });
var outcome = Await(cut, navigation);

Assert.AreEqual(BrouterNavigationStatus.Redirected, outcome.Status);
Assert.AreEqual("/b", outcome.RedirectedTo);
cut.WaitForAssertion(() => cut.Find("[data-testid=b]"));
}

[TestMethod]
public void Failing_loader_resolves_Failed_with_the_exception()
{
var (cut, brouter) = RenderAtA();

ValueTask<BrouterNavigationOutcome> navigation = default;
cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/fail"); });
var outcome = Await(cut, navigation);

Assert.AreEqual(BrouterNavigationStatus.Failed, outcome.Status);
Assert.IsInstanceOfType<InvalidOperationException>(outcome.Exception);
Assert.IsTrue(outcome.Exception!.Message.Contains("loader exploded"));
}

[TestMethod]
public void Unmatched_url_resolves_NotFound()
{
var (cut, brouter) = RenderAtA();

ValueTask<BrouterNavigationOutcome> navigation = default;
cut.InvokeAsync(() => { navigation = brouter.NavigateAsync("/nope/nothing/here"); });
var outcome = Await(cut, navigation);

Assert.AreEqual(BrouterNavigationStatus.NotFound, outcome.Status);
}

[TestMethod]
public void A_newer_navigation_supersedes_an_in_flight_awaiter()
{
var (cut, brouter) = RenderAtA();

// First navigation parks inside the /slow loader, genuinely in-flight...
ValueTask<BrouterNavigationOutcome> first = default;
cut.InvokeAsync(() => { first = brouter.NavigateAsync("/slow"); });

// ...then a second navigation starts before the first's loader finishes.
ValueTask<BrouterNavigationOutcome> second = default;
cut.InvokeAsync(() => { second = brouter.NavigateAsync("/b"); });

var firstOutcome = Await(cut, first);
var secondOutcome = Await(cut, second);
cut.Instance.SlowGate.TrySetResult(); // release the parked loader so nothing leaks

Assert.AreEqual(BrouterNavigationStatus.Superseded, firstOutcome.Status);
Assert.AreEqual(BrouterNavigationStatus.Succeeded, secondOutcome.Status);
cut.WaitForAssertion(() => cut.Find("[data-testid=b]"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Await the InvokeAsync dispatch before reading navigation. cut.InvokeAsync(...) is fire-and-forget here, so Await(cut, navigation) can see the default ValueTask<BrouterNavigationOutcome> and return a bogus completed result. Make the tests async Task and await the dispatch before calling Await.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Brouter/Tests/Bit.Brouter.Tests/NavigateAsyncTests.cs` around lines 20 -
129, The test methods in NavigateAsyncTests are reading the navigation ValueTask
before the cut.InvokeAsync dispatch has completed, so navigation can still be
the default value and produce a bogus outcome. Update the affected tests to be
async Task methods, await cut.InvokeAsync when assigning navigation from
BrouterNavigation.NavigateAsync, and then call Await only after the dispatch has
finished so the real ValueTask is observed.

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.

The Brouter improvements

1 participant