diff --git a/CHANGELOG.md b/CHANGELOG.md
index b92a5588e..51d50321c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,7 +8,8 @@ All notable changes to **bUnit** will be documented in this file. The project ad
### Added
- Extension packages (`bunit.generators` and `bunit.web.query`) are flagged as stable.
-
+- Implemented feature to map route templates to parameters using NavigationManager. This allows parameters to be set based on the route template when navigating to a new location. Reported by [JamesNK](https://github.com/JamesNK) in [#1580](https://github.com/bUnit-dev/bUnit/issues/1580). By [@linkdotnet](https://github.com/linkdotnet).
+-
## [1.34.0] - 2024-11-01
### Fixed
diff --git a/docs/site/docs/providing-input/passing-parameters-to-components.md b/docs/site/docs/providing-input/passing-parameters-to-components.md
index a69b91875..e9fedcfa8 100644
--- a/docs/site/docs/providing-input/passing-parameters-to-components.md
+++ b/docs/site/docs/providing-input/passing-parameters-to-components.md
@@ -501,6 +501,41 @@ A simple example of how to test a component that receives parameters from the qu
}
```
+## Setting parameters via routing
+In Blazor, components can receive parameters via routing. This is particularly useful for passing data to components based on the URL. To enable this, the component parameters need to be annotated with the `[Parameter]` attribute and the `@page` directive (or `RouteAttribute` in code behind files).
+
+An example component that receives parameters via routing:
+
+```razor
+@page "/counter/{initialCount:int}"
+
+
Count: @InitialCount
+
+@code {
+ [Parameter]
+ public int InitialCount { get; set; }
+}
+```
+
+To test a component that receives parameters via routing, set the parameters using the `NavigationManager`:
+
+```razor
+@inherits TestContext
+
+@code {
+ [Fact]
+ public void Component_receives_parameters_from_route()
+ {
+ var cut = RenderComponent();
+ var navigationManager = Services.GetRequiredService();
+
+ navigationManager.NavigateTo("/counter/123");
+
+ cut.Find("p").TextContent.ShouldBe("Count: 123");
+ }
+}
+```
+
## Further Reading
-
diff --git a/src/bunit.core/Extensions/TestContextBaseRenderExtensions.cs b/src/bunit.core/Extensions/TestContextBaseRenderExtensions.cs
index be5f03782..2f7db9a4a 100644
--- a/src/bunit.core/Extensions/TestContextBaseRenderExtensions.cs
+++ b/src/bunit.core/Extensions/TestContextBaseRenderExtensions.cs
@@ -21,7 +21,10 @@ public static IRenderedComponentBase RenderInsideRenderTree(baseResult);
+ var component = testContext.Renderer.FindComponent(baseResult);
+ var registry = testContext.Services.GetRequiredService();
+ registry.Register(component.Instance);
+ return component;
}
///
diff --git a/src/bunit.core/Rendering/ComponentRegistry.cs b/src/bunit.core/Rendering/ComponentRegistry.cs
new file mode 100644
index 000000000..ebc333012
--- /dev/null
+++ b/src/bunit.core/Rendering/ComponentRegistry.cs
@@ -0,0 +1,26 @@
+namespace Bunit.Rendering;
+
+///
+/// This internal class is used to keep track of all components that have been rendered.
+/// This class is not intended to be used directly by users of bUnit.
+///
+public sealed class ComponentRegistry
+{
+ private readonly HashSet components = [];
+
+ ///
+ /// Retrieves all components that have been rendered.
+ ///
+ public ISet Components => components;
+
+ ///
+ /// Registers a component as rendered.
+ ///
+ public void Register(IComponent component)
+ => components.Add(component);
+
+ ///
+ /// Removes all components from the registry.
+ ///
+ public void Clear() => components.Clear();
+}
diff --git a/src/bunit.core/Rendering/TestRenderer.cs b/src/bunit.core/Rendering/TestRenderer.cs
index 61d4deecf..c3a12f2a8 100644
--- a/src/bunit.core/Rendering/TestRenderer.cs
+++ b/src/bunit.core/Rendering/TestRenderer.cs
@@ -27,6 +27,7 @@ public class TestRenderer : Renderer, ITestRenderer
private readonly List rootComponents = new();
private readonly ILogger logger;
private readonly IRenderedComponentActivator activator;
+ private readonly ComponentRegistry registry;
private bool disposed;
private TaskCompletionSource unhandledExceptionTsc = new(TaskCreationOptions.RunContinuationsAsynchronously);
private Exception? capturedUnhandledException;
@@ -68,31 +69,34 @@ private bool IsBatchInProgress
///
/// Initializes a new instance of the class.
///
- public TestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory)
+ public TestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ComponentRegistry registry, ILoggerFactory loggerFactory)
: base(services, loggerFactory)
{
logger = loggerFactory.CreateLogger();
this.activator = renderedComponentActivator;
+ this.registry = registry;
}
#elif NET5_0_OR_GREATER
///
/// Initializes a new instance of the class.
///
- public TestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory)
+ public TestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ComponentRegistry registry, ILoggerFactory loggerFactory)
: base(services, loggerFactory, new BunitComponentActivator(services, services.GetRequiredService(), null))
{
logger = loggerFactory.CreateLogger();
this.activator = renderedComponentActivator;
+ this.registry = registry;
}
///
/// Initializes a new instance of the class.
///
- public TestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
+ public TestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, ComponentRegistry registry, IComponentActivator componentActivator)
: base(services, loggerFactory, new BunitComponentActivator(services, services.GetRequiredService(), componentActivator))
{
logger = loggerFactory.CreateLogger();
this.activator = renderedComponentActivator;
+ this.registry = registry;
}
#endif
@@ -211,6 +215,7 @@ public void DisposeComponents()
});
rootComponents.Clear();
+ registry.Clear();
AssertNoUnhandledExceptions();
}
}
diff --git a/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs b/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs
index a52b1e002..be1e34f0c 100644
--- a/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs
+++ b/src/bunit.web/Asserting/MarkupMatchesAssertExtensions.cs
@@ -305,6 +305,7 @@ public static void MarkupMatches(this IRenderedFragment actual, RenderFragment e
using var renderer = new TestRenderer(
actual.Services.GetRequiredService(),
actual.Services.GetRequiredService(),
+ actual.Services.GetRequiredService(),
actual.Services.GetRequiredService());
var renderedFragment = (IRenderedFragment)renderer.RenderFragment(expected);
MarkupMatches(actual, renderedFragment, userMessage);
diff --git a/src/bunit.web/Extensions/TestServiceProviderExtensions.cs b/src/bunit.web/Extensions/TestServiceProviderExtensions.cs
index e23f5b002..3e4f0f735 100644
--- a/src/bunit.web/Extensions/TestServiceProviderExtensions.cs
+++ b/src/bunit.web/Extensions/TestServiceProviderExtensions.cs
@@ -45,6 +45,9 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
services.AddSingleton();
services.AddSingleton(s => s.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton();
+
#if NET8_0_OR_GREATER
// bUnits fake ScrollToLocationHash
services.AddSingleton();
diff --git a/src/bunit.web/Rendering/WebTestRenderer.cs b/src/bunit.web/Rendering/WebTestRenderer.cs
index 0e710b50a..65170d25f 100644
--- a/src/bunit.web/Rendering/WebTestRenderer.cs
+++ b/src/bunit.web/Rendering/WebTestRenderer.cs
@@ -18,8 +18,8 @@ public class WebTestRenderer : TestRenderer
///
/// Initializes a new instance of the class.
///
- public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory)
- : base(renderedComponentActivator, services, loggerFactory)
+ public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ComponentRegistry componentRegistry, ILoggerFactory loggerFactory)
+ : base(renderedComponentActivator, services, componentRegistry, loggerFactory)
{
#if NET5_0_OR_GREATER
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService());
@@ -30,10 +30,10 @@ public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, T
///
/// Initializes a new instance of the class.
///
- public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
- : base(renderedComponentActivator, services, loggerFactory, componentActivator)
+ public WebTestRenderer(IRenderedComponentActivator renderedComponentActivator, TestServiceProvider services, ComponentRegistry componentRegistry, ILoggerFactory loggerFactory, IComponentActivator componentActivator)
+ : base(renderedComponentActivator, services, loggerFactory, componentRegistry, componentActivator)
{
ElementReferenceContext = new WebElementReferenceContext(services.GetRequiredService());
}
#endif
-}
\ No newline at end of file
+}
diff --git a/src/bunit.web/TestContext.cs b/src/bunit.web/TestContext.cs
index 315aa4ddf..d7b554210 100644
--- a/src/bunit.web/TestContext.cs
+++ b/src/bunit.web/TestContext.cs
@@ -64,8 +64,8 @@ public virtual IRenderedComponent RenderComponent(Action
/// The render fragment to render.
/// The .
public virtual IRenderedComponent Render(RenderFragment renderFragment)
- where TComponent : IComponent
- => (IRenderedComponent)this.RenderInsideRenderTree(renderFragment);
+ where TComponent : IComponent =>
+ (IRenderedComponent)this.RenderInsideRenderTree(renderFragment);
///
/// Renders the and returns it as a .
@@ -86,13 +86,14 @@ protected override ITestRenderer CreateTestRenderer()
{
var renderedComponentActivator = Services.GetRequiredService();
var logger = Services.GetRequiredService();
+ var componentRegistry = Services.GetRequiredService();
#if !NET5_0_OR_GREATER
- return new WebTestRenderer(renderedComponentActivator, Services, logger);
+ return new WebTestRenderer(renderedComponentActivator, Services, componentRegistry, logger);
#else
var componentActivator = Services.GetService();
return componentActivator is null
- ? new WebTestRenderer(renderedComponentActivator, Services, logger)
- : new WebTestRenderer(renderedComponentActivator, Services, logger, componentActivator);
+ ? new WebTestRenderer(renderedComponentActivator, Services, componentRegistry, logger)
+ : new WebTestRenderer(renderedComponentActivator, Services, componentRegistry, logger, componentActivator);
#endif
}
diff --git a/src/bunit.web/TestDoubles/NavigationManager/ComponentRouteParameterService.cs b/src/bunit.web/TestDoubles/NavigationManager/ComponentRouteParameterService.cs
new file mode 100644
index 000000000..ec51d5026
--- /dev/null
+++ b/src/bunit.web/TestDoubles/NavigationManager/ComponentRouteParameterService.cs
@@ -0,0 +1,122 @@
+using System.Globalization;
+using System.Reflection;
+using Bunit.Rendering;
+#if NET6_0_OR_GREATER
+using ParameterViewDictionary = System.Collections.Generic.Dictionary;
+#else
+using ParameterViewDictionary = System.Collections.Generic.Dictionary;
+#endif
+
+namespace Bunit.TestDoubles;
+
+///
+/// This is an internal service that is used to update components with route parameters.
+/// It is not meant to be used outside bUnit internal classes.
+///
+public sealed class ComponentRouteParameterService
+{
+ private readonly ComponentRegistry componentRegistry;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ComponentRouteParameterService(ComponentRegistry componentRegistry)
+ {
+ this.componentRegistry = componentRegistry;
+ }
+
+ ///
+ /// Triggers the components to update their parameters based on the route parameters.
+ ///
+ public void UpdateComponentsWithRouteParameters(Uri uri)
+ {
+ _ = uri ?? throw new ArgumentNullException(nameof(uri));
+
+ var relativeUri = uri.PathAndQuery;
+
+ foreach (var instance in componentRegistry.Components)
+ {
+ var routeAttributes = GetRouteAttributesFromComponent(instance);
+
+ if (routeAttributes.Length == 0)
+ {
+ continue;
+ }
+
+ foreach (var template in routeAttributes.Select(r => r.Template))
+ {
+ var parameters = GetParametersFromTemplateAndUri(template, relativeUri, instance);
+ if (parameters.Count > 0)
+ {
+ instance.SetParametersAsync(ParameterView.FromDictionary(parameters));
+ }
+ }
+ }
+ }
+
+ private static RouteAttribute[] GetRouteAttributesFromComponent(IComponent instance) =>
+ instance.GetType()
+ .GetCustomAttributes(typeof(RouteAttribute), true)
+ .Cast()
+ .ToArray();
+
+ private static ParameterViewDictionary GetParametersFromTemplateAndUri(string template, string relativeUri, IComponent instance)
+ {
+ var templateSegments = template.Trim('/').Split("/");
+ var uriSegments = relativeUri.Trim('/').Split("/");
+
+ if (templateSegments.Length > uriSegments.Length)
+ {
+ return [];
+ }
+
+ var parameters = new ParameterViewDictionary();
+
+ for (var i = 0; i < templateSegments.Length; i++)
+ {
+ var templateSegment = templateSegments[i];
+ if (templateSegment.StartsWith('{') && templateSegment.EndsWith('}'))
+ {
+ var parameterName = GetParameterName(templateSegment);
+ var property = GetParameterProperty(instance, parameterName);
+
+ if (property is null)
+ {
+ continue;
+ }
+
+ var isCatchAllParameter = templateSegment[1] == '*';
+ parameters[property.Name] = isCatchAllParameter
+ ? string.Join("/", uriSegments.Skip(i))
+ : GetValue(uriSegments[i], property);
+ }
+ else if (templateSegment != uriSegments[i])
+ {
+ return [];
+ }
+ }
+
+ return parameters;
+ }
+
+ private static string GetParameterName(string templateSegment) =>
+ templateSegment
+ .Trim('{', '}', '*')
+ .Replace("?", string.Empty, StringComparison.OrdinalIgnoreCase)
+ .Split(':')[0];
+
+ private static PropertyInfo? GetParameterProperty(object instance, string propertyName)
+ {
+ var propertyInfos = instance.GetType()
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+ return Array.Find(propertyInfos, prop => prop.GetCustomAttributes(typeof(ParameterAttribute), true).Any() &&
+ string.Equals(prop.Name, propertyName, StringComparison.OrdinalIgnoreCase));
+ }
+
+ private static object GetValue(string value, PropertyInfo property)
+ {
+ var underlyingType = Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType;
+ return Convert.ChangeType(value, underlyingType, CultureInfo.InvariantCulture);
+ }
+}
diff --git a/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs b/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs
index 574807cf3..6566d255a 100644
--- a/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs
+++ b/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs
@@ -12,6 +12,7 @@ namespace Bunit.TestDoubles;
public sealed class FakeNavigationManager : NavigationManager
{
private readonly TestContextBase testContextBase;
+ private readonly ComponentRouteParameterService componentRouteParameterService;
private readonly Stack history = new();
///
@@ -27,9 +28,10 @@ public sealed class FakeNavigationManager : NavigationManager
/// Initializes a new instance of the class.
///
[SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "By design. Fake navigation manager defaults to local host as base URI.")]
- public FakeNavigationManager(TestContextBase testContextBase)
+ public FakeNavigationManager(TestContextBase testContextBase, ComponentRouteParameterService componentRouteParameterService)
{
this.testContextBase = testContextBase;
+ this.componentRouteParameterService = componentRouteParameterService;
Initialize("http://localhost/", "http://localhost/");
}
@@ -64,6 +66,8 @@ protected override void NavigateToCore(string uri, bool forceLoad)
{
BaseUri = GetBaseUri(absoluteUri);
}
+
+ componentRouteParameterService.UpdateComponentsWithRouteParameters(absoluteUri);
});
}
#endif
@@ -72,6 +76,7 @@ protected override void NavigateToCore(string uri, bool forceLoad)
///
protected override void NavigateToCore(string uri, NavigationOptions options)
{
+ _ = uri ?? throw new ArgumentNullException(nameof(uri));
var absoluteUri = GetNewAbsoluteUri(uri);
var changedBaseUri = HasDifferentBaseUri(absoluteUri);
@@ -129,6 +134,8 @@ protected override void NavigateToCore(string uri, NavigationOptions options)
{
BaseUri = GetBaseUri(absoluteUri);
}
+
+ componentRouteParameterService.UpdateComponentsWithRouteParameters(absoluteUri);
});
}
#endif
diff --git a/tests/bunit.core.tests/Rendering/TestRendererTest.cs b/tests/bunit.core.tests/Rendering/TestRendererTest.cs
index 06ba766c5..a62cfe843 100644
--- a/tests/bunit.core.tests/Rendering/TestRendererTest.cs
+++ b/tests/bunit.core.tests/Rendering/TestRendererTest.cs
@@ -519,6 +519,7 @@ public void Test211()
private ITestRenderer CreateRenderer() => new WebTestRenderer(
Services.GetRequiredService(),
Services,
+ Services.GetRequiredService(),
NullLoggerFactory.Instance);
internal sealed class LifeCycleMethodInvokeCounter : ComponentBase
diff --git a/tests/bunit.core.tests/Rendering/TestRendererTest.net5.cs b/tests/bunit.core.tests/Rendering/TestRendererTest.net5.cs
index d60e3d78e..e53b443a8 100644
--- a/tests/bunit.core.tests/Rendering/TestRendererTest.net5.cs
+++ b/tests/bunit.core.tests/Rendering/TestRendererTest.net5.cs
@@ -24,6 +24,7 @@ public void Test1000()
Services.GetService(),
Services,
NullLoggerFactory.Instance,
+ Services.GetRequiredService(),
activatorMock);
renderer.RenderComponent(new ComponentParameterCollection());
diff --git a/tests/bunit.web.tests/TestDoubles/NavigationManager/RouterTests.Components.cs b/tests/bunit.web.tests/TestDoubles/NavigationManager/RouterTests.Components.cs
new file mode 100644
index 000000000..0a7c182d4
--- /dev/null
+++ b/tests/bunit.web.tests/TestDoubles/NavigationManager/RouterTests.Components.cs
@@ -0,0 +1,144 @@
+namespace Bunit.TestDoubles;
+
+public partial class RouterTests : TestContext
+{
+ [Route("/page/{count:int}/{name}")]
+ private sealed class ComponentWithPageAttribute : ComponentBase
+ {
+ [Parameter] public int Count { get; set; }
+ [Parameter] public string Name { get; set; }
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "p");
+ builder.AddContent(1, Count);
+ builder.AddContent(2, " / ");
+ builder.AddContent(3, Name);
+ builder.CloseElement();
+ }
+ }
+
+ [Route("/page")]
+ [Route("/page/{count:int}")]
+ private sealed class ComponentWithMultiplePageAttributes : ComponentBase
+ {
+ [Parameter] public int Count { get; set; }
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "p");
+ builder.AddContent(1, Count);
+ builder.CloseElement();
+ }
+ }
+
+ [Route("/page/{count:int}")]
+ private sealed class ComponentWithOtherParameters : ComponentBase
+ {
+ [Parameter] public int Count { get; set; }
+ [Parameter] public int OtherNumber { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "p");
+ builder.AddContent(1, Count);
+ builder.AddContent(2, "/");
+ builder.AddContent(3, OtherNumber);
+ builder.CloseElement();
+ }
+ }
+
+ [Route("/page/{*pageRoute}")]
+ private sealed class ComponentWithCatchAllRoute : ComponentBase
+ {
+ [Parameter] public string PageRoute { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "p");
+ builder.AddContent(1, PageRoute);
+ builder.CloseElement();
+ }
+ }
+
+ [Route("/page/{count:int}")]
+ private sealed class ComponentWithCustomOnParametersSetAsyncsCall : ComponentBase
+ {
+ [Parameter] public int Count { get; set; }
+ [Parameter] public int IncrementOnParametersSet { get; set; }
+
+ protected override void OnParametersSet()
+ {
+ Count += IncrementOnParametersSet;
+ }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "p");
+ builder.AddContent(1, Count);
+ builder.CloseElement();
+ }
+ }
+
+ [Route("/page/{count?:int}")]
+ private sealed class ComponentWithOptionalParameter : ComponentBase
+ {
+ [Parameter] public int? Count { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "p");
+ builder.AddContent(1, Count);
+ builder.CloseElement();
+ }
+ }
+
+ [Route("/page/{count:int}")]
+ private sealed class ComponentThatNavigatesToSelfOnButtonClick : ComponentBase
+ {
+ [Parameter] public int Count { get; set; }
+
+ [Inject] private NavigationManager NavigationManager { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "button");
+ builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, () => NavigationManager.NavigateTo($"/page/{Count + 1}")));
+ builder.AddContent(2, "Increment");
+ builder.CloseElement();
+ builder.OpenElement(3, "p");
+ builder.AddContent(4, Count);
+ builder.CloseElement();
+ }
+ }
+
+#if NET7_0_OR_GREATER
+ [Route("/page/{count:int}")]
+ private sealed class ComponentThatNavigatesToSelfOnButtonClickIntercepted : ComponentBase
+ {
+ [Parameter] public int Count { get; set; }
+
+ [Inject] private NavigationManager NavigationManager { get; set; }
+
+ protected override void BuildRenderTree(RenderTreeBuilder builder)
+ {
+ builder.OpenElement(0, "button");
+ builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, () => NavigationManager.NavigateTo($"/page/{Count + 1}")));
+ builder.AddContent(2, "Increment");
+ builder.CloseElement();
+ builder.OpenElement(3, "p");
+ builder.AddContent(4, Count);
+ builder.CloseElement();
+ builder.OpenComponent(5);
+ builder.AddAttribute(6, "OnBeforeInternalNavigation",
+ EventCallback.Factory.Create(this,
+ InterceptNavigation
+ ));
+ builder.CloseComponent();
+ }
+
+ private static void InterceptNavigation(LocationChangingContext context)
+ {
+ context.PreventNavigation();
+ }
+ }
+#endif
+}
diff --git a/tests/bunit.web.tests/TestDoubles/NavigationManager/RouterTests.cs b/tests/bunit.web.tests/TestDoubles/NavigationManager/RouterTests.cs
new file mode 100644
index 000000000..3adab3ae1
--- /dev/null
+++ b/tests/bunit.web.tests/TestDoubles/NavigationManager/RouterTests.cs
@@ -0,0 +1,95 @@
+namespace Bunit.TestDoubles;
+
+public partial class RouterTests : TestContext
+{
+ [Fact]
+ public void NavigatingToRouteWithPageAttributeShouldSetParameters()
+ {
+ var cut = RenderComponent();
+ var navigationManager = cut.Services.GetRequiredService();
+
+ navigationManager.NavigateTo("/page/1/test");
+
+ cut.Find("p").TextContent.ShouldBe("1 / test");
+ }
+
+ [Fact]
+ public void ShouldParseMultiplePageAttributes()
+ {
+ var cut = RenderComponent();
+ var navigationManager = cut.Services.GetRequiredService();
+
+ navigationManager.NavigateTo("/page/1");
+
+ cut.Find("p").TextContent.ShouldBe("1");
+ }
+
+ [Fact]
+ public void WhenParameterIsSetNavigatingShouldNotResetNonPageAttributeParameters()
+ {
+ var cut = RenderComponent(p => p.Add(ps => ps.OtherNumber, 2));
+ var navigationManager = cut.Services.GetRequiredService();
+
+ navigationManager.NavigateTo("/page/1");
+
+ cut.Find("p").TextContent.ShouldBe("1/2");
+ }
+
+ [Fact]
+ public void GivenACatchAllRouteShouldSetParameter()
+ {
+ var cut = RenderComponent();
+ var navigationManager = cut.Services.GetRequiredService();
+
+ navigationManager.NavigateTo("/page/1/2/3");
+
+ cut.Find("p").TextContent.ShouldBe("1/2/3");
+ }
+
+ [Fact]
+ public void ShouldInvokeParameterLifeCycleEvents()
+ {
+ var cut = RenderComponent(
+ p => p.Add(ps => ps.IncrementOnParametersSet, 10));
+ var navigationManager = cut.Services.GetRequiredService();
+
+ navigationManager.NavigateTo("/page/1");
+
+ cut.Find("p").TextContent.ShouldBe("11");
+ }
+
+ [Theory]
+ [InlineData("/page/1", "1")]
+ [InlineData("/page", "")]
+ public void ShouldSetOptionalParameter(string uri, string expectedTextContent)
+ {
+ var cut = RenderComponent();
+ var navigationManager = cut.Services.GetRequiredService();
+
+ navigationManager.NavigateTo(uri);
+
+ cut.Find("p").TextContent.ShouldBe(expectedTextContent);
+ }
+
+ [Fact]
+ public void ComponentThatNavigatesToSelfOnClickShouldBeUpdated()
+ {
+ var cut = RenderComponent();
+
+ cut.Find("button").Click();
+
+ cut.Find("p").TextContent.ShouldBe("1");
+ }
+
+#if NET7_0_OR_GREATER
+ [Fact]
+ public void ComponentThatInterceptsNavigationShouldNotBeUpdated()
+ {
+ var cut = RenderComponent();
+
+ cut.Find("button").Click();
+
+ cut.Find("p").TextContent.ShouldBe("0");
+ }
+#endif
+}