Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Components/Components/src/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#nullable enable
*REMOVED*Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties) -> void
Microsoft.AspNetCore.Components.ResourceAsset.ResourceAsset(string! url, System.Collections.Generic.IReadOnlyList<Microsoft.AspNetCore.Components.ResourceAssetProperty!>? properties = null) -> void
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type!
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.get -> System.Type?
Microsoft.AspNetCore.Components.Routing.Router.NotFoundPage.set -> void
Microsoft.AspNetCore.Components.Infrastructure.ComponentsMetricsServiceCollectionExtensions
Microsoft.AspNetCore.Components.NavigationManager.OnNotFound -> System.EventHandler<Microsoft.AspNetCore.Components.Routing.NotFoundEventArgs!>!
Expand Down
7 changes: 6 additions & 1 deletion src/Components/Components/src/Routing/Router.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ static readonly IReadOnlyDictionary<string, object> _emptyParametersDictionary
/// </summary>
[Parameter]
[DynamicallyAccessedMembers(LinkerFlags.Component)]
public Type NotFoundPage { get; set; } = default!;
public Type? NotFoundPage { get; set; }

/// <summary>
/// Gets or sets the content to display when a match is found for the requested route.
Expand Down Expand Up @@ -141,6 +141,11 @@ public async Task SetParametersAsync(ParameterView parameters)
throw new InvalidOperationException($"The {nameof(Router)} component requires a value for the parameter {nameof(Found)}.");
}

if (NotFound != null && NotFoundPage != null)
{
throw new InvalidOperationException($"The {nameof(Router)} component cannot have both {nameof(NotFound)} and {nameof(NotFoundPage)} parameters set. Use either {nameof(NotFound)} render fragment or {nameof(NotFoundPage)} component type, but not both.");
}

if (NotFoundPage != null)
{
if (!typeof(IComponent).IsAssignableFrom(NotFoundPage))
Expand Down
70 changes: 70 additions & 0 deletions src/Components/Components/test/Routing/RouterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,73 @@ await _renderer.Dispatcher.InvokeAsync(() =>
Assert.Equal("Not found", renderedFrame.TextContent);
}

[Fact]
public async Task ThrowsIfBothNotFoundAndNotFoundPageAreSet()
{
// Arrange
var parameters = new Dictionary<string, object>
{
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
{ nameof(Router.NotFound), (RenderFragment)(builder => builder.AddContent(0, "Custom content")) },
{ nameof(Router.NotFoundPage), typeof(TestNotFoundPageComponent) },
};

// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(async () =>
await _renderer.Dispatcher.InvokeAsync(() =>
_router.SetParametersAsync(ParameterView.FromDictionary(parameters))));

Assert.Contains("cannot have both NotFound and NotFoundPage parameters set", exception.Message);
}

[Fact]
public async Task UsesNotFoundPageIfSpecified()
{
// Arrange
_navigationManager.NotifyLocationChanged("https://www.example.com/subdir/nonexistent", false);
var parameters = new Dictionary<string, object>
{
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
{ nameof(Router.NotFoundPage), typeof(TestNotFoundPageComponent) },
};

// Act
await _renderer.Dispatcher.InvokeAsync(() =>
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));

// Assert
var batch = _renderer.Batches.First();
var renderedFrame = batch.ReferenceFrames.Skip(1).First(); // Skip the RouteView frame
Assert.Equal(RenderTreeFrameType.Attribute, renderedFrame.FrameType);
Assert.Equal(nameof(RouteView.RouteData), renderedFrame.AttributeName);
var routeData = (RouteData)renderedFrame.AttributeValue;
Assert.Equal(typeof(TestNotFoundPageComponent), routeData.PageType);
}

[Fact]
public async Task UsesNotFoundPageOverNotFound()
{
// Arrange - This test verifies that if only NotFoundPage is set (not both), it takes precedence over default
_navigationManager.NotifyLocationChanged("https://www.example.com/subdir/nonexistent", false);
var parameters = new Dictionary<string, object>
{
{ nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
{ nameof(Router.NotFoundPage), typeof(TestNotFoundPageComponent) },
};

// Act
await _renderer.Dispatcher.InvokeAsync(() =>
_router.SetParametersAsync(ParameterView.FromDictionary(parameters)));

// Assert
var batch = _renderer.Batches.First();
var renderedFrame = batch.ReferenceFrames.Skip(1).First(); // Skip the RouteView frame
Assert.Equal(RenderTreeFrameType.Attribute, renderedFrame.FrameType);
Assert.Equal(nameof(RouteView.RouteData), renderedFrame.AttributeName);
var routeData = (RouteData)renderedFrame.AttributeValue;
Assert.Equal(typeof(TestNotFoundPageComponent), routeData.PageType);
}

internal class TestNavigationManager : NavigationManager
{
public TestNavigationManager() =>
Expand Down Expand Up @@ -306,4 +373,7 @@ public class MatchAnythingComponent : ComponentBase { }

[Route("a/b/c")]
public class MultiSegmentRouteComponent : ComponentBase { }

[Route("/not-found")]
public class TestNotFoundPageComponent : ComponentBase { }
}
Loading