File: Routing\RouterTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
 
namespace Microsoft.AspNetCore.Components.Routing;
 
public class RouterTest
{
    private readonly Router _router;
    private readonly TestNavigationManager _navigationManager;
    private readonly TestRenderer _renderer;
 
    public RouterTest()
    {
        var services = new ServiceCollection();
        _navigationManager = new TestNavigationManager();
        services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
        services.AddSingleton<NavigationManager>(_navigationManager);
        services.AddSingleton<INavigationInterception, TestNavigationInterception>();
        services.AddSingleton<IScrollToLocationHash, TestScrollToLocationHash>();
        var serviceProvider = services.BuildServiceProvider();
 
        _renderer = new TestRenderer(serviceProvider);
        _renderer.ShouldHandleExceptions = true;
        _router = (Router)_renderer.InstantiateComponent<Router>();
        _router.AppAssembly = Assembly.GetExecutingAssembly();
        _router.Found = routeData => (builder) => builder.AddContent(0, $"Rendering route matching {routeData.PageType}");
        _renderer.AssignRootComponentId(_router);
    }
 
    [Fact]
    public async Task CanRunOnNavigateAsync()
    {
        // Arrange
        var called = false;
        Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
        {
            await Task.CompletedTask;
            called = true;
        };
        _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
 
        // Act
        await _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
 
        // Assert
        Assert.True(called);
    }
 
    [Fact]
    public async Task CanceledFailedOnNavigateAsyncDoesNothing()
    {
        // Arrange
        var onNavigateInvoked = 0;
        Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
        {
            onNavigateInvoked += 1;
            if (args.Path.EndsWith("jan", StringComparison.Ordinal))
            {
                await Task.Delay(Timeout.Infinite, args.CancellationToken);
                throw new Exception("This is an uncaught exception.");
            }
        };
        var refreshCalled = 0;
        _renderer.OnUpdateDisplay = (renderBatch) =>
        {
            refreshCalled += 1;
            return;
        };
        _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
 
        // Act
        var janTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
        var febTask = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
 
        await janTask;
        await febTask;
 
        // Assert that we render the second route component and don't throw an exception
        Assert.Empty(_renderer.HandledExceptions);
        Assert.Equal(2, onNavigateInvoked);
        Assert.Equal(2, refreshCalled);
    }
 
    [Fact]
    public async Task AlreadyCanceledOnNavigateAsyncDoesNothing()
    {
        // Arrange
        var triggerCancel = new TaskCompletionSource();
        Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
        {
            if (args.Path.EndsWith("jan", StringComparison.Ordinal))
            {
                var tcs = new TaskCompletionSource();
                await triggerCancel.Task;
                tcs.TrySetCanceled();
                await tcs.Task;
            }
        };
        var refreshCalled = false;
        _renderer.OnUpdateDisplay = (renderBatch) =>
        {
            if (!refreshCalled)
            {
                Assert.True(true);
                return;
            }
            Assert.Fail("OnUpdateDisplay called more than once.");
        };
        _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
 
        // Act (start the operations then await them)
        var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
        var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
        triggerCancel.TrySetResult();
 
        await jan;
        await feb;
    }
 
    [Fact]
    public void CanCancelPreviousOnNavigateAsync()
    {
        // Arrange
        var cancelled = "";
        Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
        {
            await Task.CompletedTask;
            args.CancellationToken.Register(() => cancelled = args.Path);
        };
        _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
 
        // Act
        _ = _router.RunOnNavigateAsync("jan", false);
        _ = _router.RunOnNavigateAsync("feb", false);
 
        // Assert
        var expected = "jan";
        Assert.Equal(expected, cancelled);
    }
 
    [Fact]
    public async Task RefreshesOnceOnCancelledOnNavigateAsync()
    {
        // Arrange
        Action<NavigationContext> OnNavigateAsync = async (NavigationContext args) =>
        {
            if (args.Path.EndsWith("jan", StringComparison.Ordinal))
            {
                await Task.Delay(Timeout.Infinite, args.CancellationToken);
            }
        };
        var refreshCalled = false;
        _renderer.OnUpdateDisplay = (renderBatch) =>
        {
            if (!refreshCalled)
            {
                Assert.True(true);
                return;
            }
            Assert.Fail("OnUpdateDisplay called more than once.");
        };
        _router.OnNavigateAsync = new EventCallback<NavigationContext>(null, OnNavigateAsync);
 
        // Act
        var jan = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/jan", false));
        var feb = _renderer.Dispatcher.InvokeAsync(() => _router.RunOnNavigateAsync("http://example.com/feb", false));
 
        await jan;
        await feb;
    }
 
    [Fact]
    public async Task UsesCurrentRouteMatchingIfSpecified()
    {
        // Arrange
        // Current routing prefers exactly-matched patterns over {*someWildcard}, no matter
        // how many segments are in the exact match
        _navigationManager.NotifyLocationChanged("https://www.example.com/subdir/a/b/c", false);
        var parameters = new Dictionary<string, object>
            {
                { nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
                { nameof(Router.NotFound), (RenderFragment)(builder => { }) },
            };
 
        // Act
        await _renderer.Dispatcher.InvokeAsync(() =>
            _router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
 
        // Assert
        var renderedFrame = _renderer.Batches.First().ReferenceFrames.First();
        Assert.Equal(RenderTreeFrameType.Text, renderedFrame.FrameType);
        Assert.Equal($"Rendering route matching {typeof(MultiSegmentRouteComponent)}", renderedFrame.TextContent);
    }
 
    [Fact]
    public async Task SetParametersAsyncRefreshesOnce()
    {
        //Arrange
        var parameters = new Dictionary<string, object>
            {
                { nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
                { nameof(Router.NotFound), (RenderFragment)(builder => { }) },
            };
 
        var refreshCalled = 0;
        _renderer.OnUpdateDisplay = (renderBatch) =>
        {
            refreshCalled += 1;
            return;
        };
 
        // Act
        await _renderer.Dispatcher.InvokeAsync(() =>
            _router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
 
        //Assert
        Assert.Equal(1, refreshCalled);
    }
 
    [Fact]
    public async Task UsesNotFoundContentIfSpecified()
    {
        // Arrange
        _navigationManager.NotifyLocationChanged("https://www.example.com/subdir/nonexistent", false);
        var parameters = new Dictionary<string, object>
        {
            { nameof(Router.AppAssembly), typeof(RouterTest).Assembly },
            { nameof(Router.NotFound), (RenderFragment)(builder => builder.AddContent(0, "Custom content")) },
        };
 
        // Act
        await _renderer.Dispatcher.InvokeAsync(() =>
            _router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
 
        // Assert
        var renderedFrame = _renderer.Batches.First().ReferenceFrames.First();
        Assert.Equal(RenderTreeFrameType.Text, renderedFrame.FrameType);
        Assert.Equal("Custom content", renderedFrame.TextContent);
    }
 
    [Fact]
    public async Task UsesDefaultNotFoundContentIfNotSpecified()
    {
        // Arrange
        _navigationManager.NotifyLocationChanged("https://www.example.com/subdir/nonexistent", false);
        var parameters = new Dictionary<string, object>
        {
            { nameof(Router.AppAssembly), typeof(RouterTest).Assembly }
        };
 
        // Act
        await _renderer.Dispatcher.InvokeAsync(() =>
            _router.SetParametersAsync(ParameterView.FromDictionary(parameters)));
 
        // Assert
        var renderedFrame = _renderer.Batches.First().ReferenceFrames.First();
        Assert.Equal(RenderTreeFrameType.Text, renderedFrame.FrameType);
        Assert.Equal("Not found", renderedFrame.TextContent);
    }
 
    internal class TestNavigationManager : NavigationManager
    {
        public TestNavigationManager() =>
            Initialize("https://www.example.com/subdir/", "https://www.example.com/subdir/jan");
 
        public void NotifyLocationChanged(string uri, bool intercepted, string state = null)
        {
            Uri = uri;
            NotifyLocationChanged(intercepted);
        }
    }
 
    internal sealed class TestNavigationInterception : INavigationInterception
    {
        public static readonly TestNavigationInterception Instance = new TestNavigationInterception();
 
        public Task EnableNavigationInterceptionAsync()
        {
            return Task.CompletedTask;
        }
    }
 
    internal sealed class TestScrollToLocationHash : IScrollToLocationHash
    {
        public Task RefreshScrollPositionForHash(string locationAbsolute)
        {
            return Task.CompletedTask;
        }
    }
 
    [Route("feb")]
    public class FebComponent : ComponentBase { }
 
    [Route("jan")]
    public class JanComponent : ComponentBase { }
 
    [Route("a/{*matchAnything}")]
    public class MatchAnythingComponent : ComponentBase { }
 
    [Route("a/b/c")]
    public class MultiSegmentRouteComponent : ComponentBase { }
}