Skip to content

Commit 3cb8218

Browse files
committed
Add tests + throw if path does not match any component.
1 parent 4a88e36 commit 3cb8218

File tree

2 files changed

+202
-7
lines changed

2 files changed

+202
-7
lines changed

src/Components/Components/src/Routing/Router.cs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -409,21 +409,24 @@ private void OnNotFound(object sender, NotFoundEventArgs args)
409409
}
410410
}
411411

412-
private void RenderComponentByRoute(RenderTreeBuilder builder, string route)
412+
internal void RenderComponentByRoute(RenderTreeBuilder builder, string route)
413413
{
414414
var componentType = FindComponentTypeByRoute(route);
415415

416-
if (componentType != null)
416+
if (componentType is null)
417417
{
418-
builder.OpenComponent<RouteView>(0);
419-
builder.AddAttribute(1, nameof(RouteView.RouteData),
420-
new RouteData(componentType, new Dictionary<string, object>()));
421-
builder.CloseComponent();
418+
throw new InvalidOperationException($"No component found for route '{route}'. " +
419+
$"Ensure the route matches a component with a [Route] attribute.");
422420
}
421+
422+
builder.OpenComponent<RouteView>(0);
423+
builder.AddAttribute(1, nameof(RouteView.RouteData),
424+
new RouteData(componentType, new Dictionary<string, object>()));
425+
builder.CloseComponent();
423426
}
424427

425428
[return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
426-
private Type? FindComponentTypeByRoute(string route)
429+
internal Type? FindComponentTypeByRoute(string route)
427430
{
428431
RefreshRouteTable();
429432
var normalizedRoute = route.StartsWith('/') ? route : $"/{route}";

src/Components/Components/test/Routing/RouterTest.cs

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
using System.Reflection;
77
using Microsoft.AspNetCore.Components.RenderTree;
8+
using Microsoft.AspNetCore.Components.Rendering;
89
using Microsoft.AspNetCore.Components.Test.Helpers;
910
using Microsoft.Extensions.DependencyInjection;
1011
using Microsoft.Extensions.Logging;
@@ -301,6 +302,192 @@ await renderer.Dispatcher.InvokeAsync(() =>
301302
Assert.Contains("Use either NotFound or NotFoundPage", exception.Message);
302303
}
303304

305+
[Fact]
306+
public async Task OnNotFound_WithNotFoundPageSet_UsesNotFoundPage()
307+
{
308+
// Create a new router instance for this test to control Attach() timing
309+
var services = new ServiceCollection();
310+
var testNavManager = new TestNavigationManager();
311+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
312+
services.AddSingleton<NavigationManager>(testNavManager);
313+
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
314+
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
315+
var serviceProvider = services.BuildServiceProvider();
316+
317+
var testRenderer = new TestRenderer(serviceProvider);
318+
testRenderer.ShouldHandleExceptions = true;
319+
var testRouter = (Router)testRenderer.InstantiateComponent<Router>();
320+
testRouter.AppAssembly = Assembly.GetExecutingAssembly();
321+
testRouter.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
322+
323+
var parameters = new Dictionary<string, object>
324+
{
325+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
326+
{ nameof(Router.NotFoundPage), typeof(NotFoundTestComponent) }
327+
};
328+
329+
// Assign the root component ID which will call Attach()
330+
testRenderer.AssignRootComponentId(testRouter);
331+
332+
// Act
333+
await testRenderer.Dispatcher.InvokeAsync(() =>
334+
testRouter.SetParametersAsync(ParameterView.FromDictionary(parameters)));
335+
336+
// Trigger the NavigationManager's OnNotFound event
337+
await testRenderer.Dispatcher.InvokeAsync(() => testNavManager.TriggerNotFound());
338+
339+
// Assert
340+
var lastBatch = testRenderer.Batches.Last();
341+
var renderedFrame = lastBatch.ReferenceFrames.First();
342+
Assert.Equal(RenderTreeFrameType.Component, renderedFrame.FrameType);
343+
Assert.Equal(typeof(RouteView), renderedFrame.ComponentType);
344+
345+
// Verify that the RouteData contains the NotFoundTestComponent
346+
var routeViewFrame = lastBatch.ReferenceFrames.Skip(1).First();
347+
Assert.Equal(RenderTreeFrameType.Attribute, routeViewFrame.FrameType);
348+
var routeData = (RouteData)routeViewFrame.AttributeValue;
349+
Assert.Equal(typeof(NotFoundTestComponent), routeData.PageType);
350+
}
351+
352+
[Fact]
353+
public async Task OnNotFound_WithArgsPathSet_RendersComponentByRoute()
354+
{
355+
// Create a new router instance for this test to control Attach() timing
356+
var services = new ServiceCollection();
357+
var testNavManager = new TestNavigationManager();
358+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
359+
services.AddSingleton<NavigationManager>(testNavManager);
360+
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
361+
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
362+
var serviceProvider = services.BuildServiceProvider();
363+
364+
var testRenderer = new TestRenderer(serviceProvider);
365+
testRenderer.ShouldHandleExceptions = true;
366+
var testRouter = (Router)testRenderer.InstantiateComponent<Router>();
367+
testRouter.AppAssembly = Assembly.GetExecutingAssembly();
368+
testRouter.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
369+
370+
var parameters = new Dictionary<string, object>
371+
{
372+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
373+
};
374+
375+
// Subscribe to OnNotFound event BEFORE router attaches and set args.Path
376+
testNavManager.OnNotFound += (sender, args) =>
377+
{
378+
args.Path = "/jan"; // Point to an existing route
379+
};
380+
381+
// Assign the root component ID which will call Attach()
382+
testRenderer.AssignRootComponentId(testRouter);
383+
384+
// Act
385+
await testRenderer.Dispatcher.InvokeAsync(() =>
386+
testRouter.SetParametersAsync(ParameterView.FromDictionary(parameters)));
387+
388+
// Trigger the NavigationManager's OnNotFound event
389+
await testRenderer.Dispatcher.InvokeAsync(() => testNavManager.TriggerNotFound());
390+
391+
// Assert
392+
var lastBatch = testRenderer.Batches.Last();
393+
var renderedFrame = lastBatch.ReferenceFrames.First();
394+
Assert.Equal(RenderTreeFrameType.Component, renderedFrame.FrameType);
395+
Assert.Equal(typeof(RouteView), renderedFrame.ComponentType);
396+
397+
// Verify that the RouteData contains the correct component type
398+
var routeViewFrame = lastBatch.ReferenceFrames.Skip(1).First();
399+
Assert.Equal(RenderTreeFrameType.Attribute, routeViewFrame.FrameType);
400+
var routeData = (RouteData)routeViewFrame.AttributeValue;
401+
Assert.Equal(typeof(JanComponent), routeData.PageType);
402+
}
403+
404+
[Fact]
405+
public async Task OnNotFound_WithBothNotFoundPageAndArgsPath_PreferArgs()
406+
{
407+
// Create a new router instance for this test to control Attach() timing
408+
var services = new ServiceCollection();
409+
var testNavManager = new TestNavigationManager();
410+
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
411+
services.AddSingleton<NavigationManager>(testNavManager);
412+
services.AddSingleton<INavigationInterception, TestNavigationInterception>();
413+
services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
414+
var serviceProvider = services.BuildServiceProvider();
415+
416+
var testRenderer = new TestRenderer(serviceProvider);
417+
testRenderer.ShouldHandleExceptions = true;
418+
var testRouter = (Router)testRenderer.InstantiateComponent<Router>();
419+
testRouter.AppAssembly = Assembly.GetExecutingAssembly();
420+
testRouter.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
421+
422+
var parameters = new Dictionary<string, object>
423+
{
424+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
425+
{ nameof(Router.NotFoundPage), typeof(NotFoundTestComponent) }
426+
};
427+
428+
// Subscribe to OnNotFound event BEFORE router attaches and sets up its own subscription
429+
testNavManager.OnNotFound += (sender, args) =>
430+
{
431+
args.Path = "/jan"; // This should take precedence over NotFoundPage
432+
};
433+
434+
// Now assign the root component ID which will call Attach()
435+
testRenderer.AssignRootComponentId(testRouter);
436+
437+
await testRenderer.Dispatcher.InvokeAsync(() =>
438+
testRouter.SetParametersAsync(ParameterView.FromDictionary(parameters)));
439+
440+
// trigger the NavigationManager's OnNotFound event
441+
await testRenderer.Dispatcher.InvokeAsync(() => testNavManager.TriggerNotFound());
442+
443+
// The Router should have rendered using RenderComponentByRoute (args.Path) instead of NotFoundPage
444+
var lastBatch = testRenderer.Batches.Last();
445+
var renderedFrame = lastBatch.ReferenceFrames.First();
446+
Assert.Equal(RenderTreeFrameType.Component, renderedFrame.FrameType);
447+
Assert.Equal(typeof(RouteView), renderedFrame.ComponentType);
448+
449+
// Verify that the RouteData contains the JanComponent (from args.Path), not NotFoundTestComponent
450+
var routeViewFrame = lastBatch.ReferenceFrames.Skip(1).First();
451+
Assert.Equal(RenderTreeFrameType.Attribute, routeViewFrame.FrameType);
452+
var routeData = (RouteData)routeViewFrame.AttributeValue;
453+
Assert.Equal(typeof(JanComponent), routeData.PageType);
454+
}
455+
456+
[Fact]
457+
public async Task FindComponentTypeByRoute_WithValidRoute_ReturnsComponentType()
458+
{
459+
var parameters = new Dictionary<string, object>
460+
{
461+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
462+
};
463+
464+
await _renderer.Dispatcher.InvokeAsync(() =>
465+
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
466+
467+
var result = _router.FindComponentTypeByRoute("/jan");
468+
Assert.Equal(typeof(JanComponent), result);
469+
}
470+
471+
[Fact]
472+
public async Task RenderComponentByRoute_WithInvalidRoute_ThrowsException()
473+
{
474+
var parameters = new Dictionary<string, object>
475+
{
476+
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
477+
};
478+
479+
await _renderer.Dispatcher.InvokeAsync(() =>
480+
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
481+
482+
var builder = new RenderTreeBuilder();
483+
484+
var exception = Assert.Throws<InvalidOperationException>(() =>
485+
{
486+
_router.RenderComponentByRoute(builder, "/nonexistent-route");
487+
});
488+
Assert.Contains("No component found for route '/nonexistent-route'", exception.Message);
489+
}
490+
304491
internal class TestNavigationManager : NavigationManager
305492
{
306493
public TestNavigationManager() =>
@@ -311,6 +498,11 @@ public void NotifyLocationChanged(string uri, bool intercepted, string state = n
311498
Uri = uri;
312499
NotifyLocationChanged(intercepted);
313500
}
501+
502+
public void TriggerNotFound()
503+
{
504+
base.NotFound();
505+
}
314506
}
315507

316508
internal sealed class TestNavigationInterception : INavigationInterception

0 commit comments

Comments
 (0)