diff --git a/CHANGELOG.md b/CHANGELOG.md index e255a338e..ef8f65c9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ All notable changes to **bUnit** will be documented in this file. The project ad By [@linkdotnet](https://github.com/linkdotnet) and [@egil](https://github.com/egil). +- Added support for `NavigationLock`, which allows user code to intercept and prevent navigation. By [@linkdotnet](https://github.com/linkdotnet) and [@egil](https://github.com/egil). + ### Fixed - `JSInterop.VerifyInvoke` reported the wrong number of actual invocations of a given identifier. Reported by [@otori](https://github.com/otori). Fixed by [@linkdotnet](https://github.com/linkdotnet). diff --git a/docs/site/docs/test-doubles/fake-navigation-manager.md b/docs/site/docs/test-doubles/fake-navigation-manager.md index 540823b19..a12f53b70 100644 --- a/docs/site/docs/test-doubles/fake-navigation-manager.md +++ b/docs/site/docs/test-doubles/fake-navigation-manager.md @@ -71,3 +71,69 @@ Assert.Equal("http://localhost/foo", navMan.Uri); ``` If a component issues multiple `NavigateTo` calls, then it is possible to inspect the navigation history by accessing the property. It's a stack based structure, meaning the latest navigations will be first in the collection at index 0. + +## Asserting that a navigation was prevented with the `NavigationLock` component + +The `NavigationLock` component, which was introduced with .NET 7, gives the possibility to intercept the navigation and can even prevent it. bUnit will always create a history entry for prevented or even failed interceptions. This gets reflected in the property, as well as in case of an exception on the property. + +A component can look like this: +```razor +@inject NavigationManager NavigationManager + + + + + +@code { + private void InterceptNavigation(LocationChangingContext context) + { + context.PreventNavigation(); + } +} +``` + +A typical test, which asserts that the navigation got prevented, would look like this: + +```csharp +using var ctx = new TestContext(); +var navMan = ctx.Services.GetRequiredService(); +var cut = ctx.RenderComponent(); + +cut.Find("button").Click(); + +// Assert that the navigation was prevented +var navigationHistory = navMan.History.Single(); +Assert.Equal(NavigationState.Prevented, navigationHistory.NavigationState); +``` + +## Simulate preventing navigation from a `` with the `NavigationLock` component + +As `` navigation is not natively supported in bUnit, the `NavigationManager` can be used to simulate the exact behavior. + +```razor +Counter + + + +@code { + private void InterceptNavigation(LocationChangingContext context) + { + throw new Exception(); + } +} +``` + +The test utilizes the `NavigationManager` itself to achieve the same: + +```csharp +using var ctx = new TestContext(); +var navMan = ctx.Services.GetRequiredService(); +var cut = ctx.RenderComponent(); + +navMan.NavigateTo("/counter"); + +// Assert that the navigation was prevented +var navigationHistory = navMan.History.Single(); +Assert.Equal(NavigationState.Faulted, navigationHistory.NavigationState); +Assert.NotNull(navigationHistory.Exception); +``` \ No newline at end of file diff --git a/src/bunit.web/JSInterop/BunitJSInterop.cs b/src/bunit.web/JSInterop/BunitJSInterop.cs index 1625c6a1a..4c15f4a65 100644 --- a/src/bunit.web/JSInterop/BunitJSInterop.cs +++ b/src/bunit.web/JSInterop/BunitJSInterop.cs @@ -143,6 +143,10 @@ private void AddCustomNet5Handlers() private void AddCustomNet6Handlers() { AddInvocationHandler(new FocusOnNavigateHandler()); +#if NET7_0_OR_GREATER + AddInvocationHandler(new NavigationLockDisableNavigationPromptInvocationHandler()); + AddInvocationHandler(new NavigationLockEnableNavigationPromptInvocationHandler()); +#endif } #endif } \ No newline at end of file diff --git a/src/bunit.web/JSInterop/InvocationHandlers/Implementation/NavigationLockDisableNavigationPromptInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/NavigationLockDisableNavigationPromptInvocationHandler.cs new file mode 100644 index 000000000..f1740d2ce --- /dev/null +++ b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/NavigationLockDisableNavigationPromptInvocationHandler.cs @@ -0,0 +1,14 @@ +#if NET7_0_OR_GREATER +namespace Bunit.JSInterop.InvocationHandlers.Implementation; + +internal sealed class NavigationLockDisableNavigationPromptInvocationHandler : JSRuntimeInvocationHandler +{ + private const string Identifier = "Blazor._internal.NavigationLock.disableNavigationPrompt"; + + internal NavigationLockDisableNavigationPromptInvocationHandler() + : base(inv => inv.Identifier.Equals(Identifier, StringComparison.Ordinal), isCatchAllHandler: true) + { + SetVoidResult(); + } +} +#endif diff --git a/src/bunit.web/JSInterop/InvocationHandlers/Implementation/NavigationLockEnableNavigationPromptInvocationHandler.cs b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/NavigationLockEnableNavigationPromptInvocationHandler.cs new file mode 100644 index 000000000..c5015288e --- /dev/null +++ b/src/bunit.web/JSInterop/InvocationHandlers/Implementation/NavigationLockEnableNavigationPromptInvocationHandler.cs @@ -0,0 +1,14 @@ +#if NET7_0_OR_GREATER +namespace Bunit.JSInterop.InvocationHandlers.Implementation; + +internal sealed class NavigationLockEnableNavigationPromptInvocationHandler : JSRuntimeInvocationHandler +{ + private const string Identifier = "Blazor._internal.NavigationLock.enableNavigationPrompt"; + + internal NavigationLockEnableNavigationPromptInvocationHandler() + : base(inv => inv.Identifier.Equals(Identifier, StringComparison.Ordinal), isCatchAllHandler: true) + { + SetVoidResult(); + } +} +#endif diff --git a/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs b/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs index e4c146f74..a243100d5 100644 --- a/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs +++ b/src/bunit.web/TestDoubles/NavigationManager/FakeNavigationManager.cs @@ -1,4 +1,5 @@ using Bunit.Rendering; +using Microsoft.AspNetCore.Components.Routing; namespace Bunit.TestDoubles; @@ -68,7 +69,6 @@ protected override void NavigateToCore(string uri, bool forceLoad) #endif #if NET6_0_OR_GREATER - /// protected override void NavigateToCore(string uri, NavigationOptions options) { @@ -85,12 +85,37 @@ protected override void NavigateToCore(string uri, NavigationOptions options) if (options.ReplaceHistoryEntry && history.Count > 0) history.Pop(); - history.Push(new NavigationHistory(uri, options)); - +#if NET7_0_OR_GREATER + renderer.Dispatcher.InvokeAsync(async () => +#else renderer.Dispatcher.InvokeAsync(() => +#endif { Uri = absoluteUri.OriginalString; +#if NET7_0_OR_GREATER + var shouldContinueNavigation = false; + try + { + shouldContinueNavigation = await NotifyLocationChangingAsync(uri, options.HistoryEntryState, isNavigationIntercepted: false).ConfigureAwait(false); + } + catch (Exception exception) + { + history.Push(new NavigationHistory(uri, options, NavigationState.Faulted, exception)); + return; + } + + history.Push(new NavigationHistory(uri, options, shouldContinueNavigation ? NavigationState.Succeeded : NavigationState.Prevented)); + + if (!shouldContinueNavigation) + { + return; + } +#else + history.Push(new NavigationHistory(uri, options)); +#endif + + // Only notify of changes if user navigates within the same // base url (domain). Otherwise, the user navigated away // from the app, and Blazor's NavigationManager would @@ -107,6 +132,15 @@ protected override void NavigateToCore(string uri, NavigationOptions options) } #endif +#if NET7_0_OR_GREATER + /// + protected override void SetNavigationLockState(bool value) {} + + /// + protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context) + => throw ex; +#endif + private URI GetNewAbsoluteUri(string uri) => URI.IsWellFormedUriString(uri, UriKind.Relative) ? ToAbsoluteUri(uri) @@ -124,4 +158,4 @@ private static string GetBaseUri(URI uri) { return uri.Scheme + "://" + uri.Authority + "/"; } -} \ No newline at end of file +} diff --git a/src/bunit.web/TestDoubles/NavigationManager/NavigationHistory.cs b/src/bunit.web/TestDoubles/NavigationManager/NavigationHistory.cs index 91ab03763..e6b86e73c 100644 --- a/src/bunit.web/TestDoubles/NavigationManager/NavigationHistory.cs +++ b/src/bunit.web/TestDoubles/NavigationManager/NavigationHistory.cs @@ -1,3 +1,5 @@ +using Microsoft.AspNetCore.Components.Routing; + namespace Bunit.TestDoubles; /// @@ -18,27 +20,66 @@ public sealed class NavigationHistory : IEquatable public Bunit.TestDoubles.NavigationOptions Options { get; } #endif #if NET6_0_OR_GREATER - public Microsoft.AspNetCore.Components.NavigationOptions Options { get; } + public NavigationOptions Options { get; } #endif +#if NET7_0_OR_GREATER + /// + /// Gets the associated with this history entry. + /// + public NavigationState State { get; } + + /// + /// Gets the exception thrown from the handler, if any. + /// + /// + /// Will not be null when is . + /// + public Exception? Exception { get; } +#endif + +#if !NET6_0_OR_GREATER /// /// Initializes a new instance of the class. /// /// /// [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Using string to align with NavigationManager")] -#if !NET6_0_OR_GREATER public NavigationHistory(string uri, Bunit.TestDoubles.NavigationOptions options) { Uri = uri; Options = options; } #endif -#if NET6_0_OR_GREATER - public NavigationHistory(string uri, Microsoft.AspNetCore.Components.NavigationOptions options) +#if NET6_0 + /// + /// Initializes a new instance of the class. + /// + /// + /// + [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Using string to align with NavigationManager")] + public NavigationHistory(string uri, NavigationOptions options) + { + Uri = uri; + Options = options; + } +#endif + +#if NET7_0_OR_GREATER + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + [SuppressMessage("Design", "CA1054:URI-like parameters should not be strings", Justification = "Using string to align with NavigationManager")] + public NavigationHistory(string uri, NavigationOptions options, NavigationState navigationState, Exception? exception = null) { Uri = uri; Options = options; + State = navigationState; + Exception = exception; } #endif diff --git a/src/bunit.web/TestDoubles/NavigationManager/NavigationState.cs b/src/bunit.web/TestDoubles/NavigationManager/NavigationState.cs new file mode 100644 index 000000000..4f11ecb44 --- /dev/null +++ b/src/bunit.web/TestDoubles/NavigationManager/NavigationState.cs @@ -0,0 +1,24 @@ +#if NET7_0_OR_GREATER +namespace Bunit.TestDoubles; + +/// +/// Describes the possible enumerations when a navigation gets intercepted. +/// +public enum NavigationState +{ + /// + /// The navigation was successfully executed. + /// + Succeeded, + + /// + /// The navigation was prevented. + /// + Prevented, + + /// + /// The OnBeforeInternalNavigation event handler threw an exception and the navigation did not complete. + /// + Faulted +} +#endif diff --git a/tests/bunit.web.tests/TestDoubles/NavigationManager/FakeNavigationManagerTest.cs b/tests/bunit.web.tests/TestDoubles/NavigationManager/FakeNavigationManagerTest.cs index bf3505516..3709497c0 100644 --- a/tests/bunit.web.tests/TestDoubles/NavigationManager/FakeNavigationManagerTest.cs +++ b/tests/bunit.web.tests/TestDoubles/NavigationManager/FakeNavigationManagerTest.cs @@ -1,101 +1,104 @@ -namespace Bunit.TestDoubles +namespace Bunit.TestDoubles; + +using static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers; + +public class FakeNavigationManagerTest : TestContext { - public class FakeNavigationManagerTest : TestContext - { - private FakeNavigationManager CreateFakeNavigationManager() - => Services.GetRequiredService(); + private FakeNavigationManager CreateFakeNavigationManager() + => Services.GetRequiredService(); - [Fact(DisplayName = "TestContext.Services has NavigationManager registered by default as FakeNavigationManager")] - public void Test001() - { - var nm = Services.GetService(); - var fnm = Services.GetService(); + [Fact(DisplayName = "TestContext.Services has NavigationManager registered by default as FakeNavigationManager")] + public void Test001() + { + var nm = Services.GetService(); + var fnm = Services.GetService(); - nm.ShouldNotBeNull(); - fnm.ShouldNotBeNull(); - nm.ShouldBe(fnm); - } + nm.ShouldNotBeNull(); + fnm.ShouldNotBeNull(); + nm.ShouldBe(fnm); + } - [Fact(DisplayName = "FakeNavigationManager.BaseUrl is set to http://localhost/")] - public void Test002() - { - var sut = CreateFakeNavigationManager(); + [Fact(DisplayName = "FakeNavigationManager.BaseUrl is set to http://localhost/")] + public void Test002() + { + var sut = CreateFakeNavigationManager(); - sut.BaseUri.ShouldBe("http://localhost/"); - } + sut.BaseUri.ShouldBe("http://localhost/"); + } - [Theory(DisplayName = "NavigateTo with relative URI converts it to absolute and sets the Uri property ")] - [InlineData("")] - [InlineData("/")] - [InlineData("/foo")] - public void Test003(string uri) - { - var sut = CreateFakeNavigationManager(); - var expectedUri = new Uri(new Uri(sut.BaseUri, UriKind.Absolute), new Uri(uri, UriKind.Relative)); + [Theory(DisplayName = "NavigateTo with relative URI converts it to absolute and sets the Uri property ")] + [InlineData("")] + [InlineData("/")] + [InlineData("/foo")] + public void Test003(string uri) + { + var sut = CreateFakeNavigationManager(); + var expectedUri = new Uri(new Uri(sut.BaseUri, UriKind.Absolute), new Uri(uri, UriKind.Relative)); - sut.NavigateTo(uri); + sut.NavigateTo(uri); - sut.Uri.ShouldBe(expectedUri.ToString()); - } + sut.Uri.ShouldBe(expectedUri.ToString()); + } - [Theory(DisplayName = "NavigateTo with absolute URI sets the Uri property")] - [InlineData("http://localhost")] - [InlineData("http://localhost/")] - [InlineData("http://localhost/foo")] - public void Test004(string uri) - { - var sut = CreateFakeNavigationManager(); - var expectedUri = new Uri(uri, UriKind.Absolute); + [Theory(DisplayName = "NavigateTo with absolute URI sets the Uri property")] + [InlineData("http://localhost")] + [InlineData("http://localhost/")] + [InlineData("http://localhost/foo")] + public void Test004(string uri) + { + var sut = CreateFakeNavigationManager(); + var expectedUri = new Uri(uri, UriKind.Absolute); - sut.NavigateTo(uri); + sut.NavigateTo(uri); - sut.Uri.ShouldBe(expectedUri.OriginalString); - } + sut.Uri.ShouldBe(expectedUri.OriginalString); + } - [Fact(DisplayName = "NavigateTo raises the NotifyLocationChanged")] - [SuppressMessage("Major Bug", "S2259:Null pointers should not be dereferenced", Justification = "BUG in analyzer - 'actualLocationChange' is NOT null on the execution path.")] - public void Test005() - { - // arrange - LocationChangedEventArgs actualLocationChange = default; - var navigationUri = "foo"; - var sut = CreateFakeNavigationManager(); - sut.LocationChanged += Sut_LocationChanged; + [Fact(DisplayName = "NavigateTo raises the NotifyLocationChanged")] + [SuppressMessage("Major Bug", "S2259:Null pointers should not be dereferenced", + Justification = "BUG in analyzer - 'actualLocationChange' is NOT null on the execution path.")] + public void Test005() + { + // arrange + LocationChangedEventArgs actualLocationChange = default; + var navigationUri = "foo"; + var sut = CreateFakeNavigationManager(); + sut.LocationChanged += Sut_LocationChanged; - // act - sut.NavigateTo(navigationUri); + // act + sut.NavigateTo(navigationUri); - // assert - actualLocationChange.Location.ShouldBe($"{sut.BaseUri}{navigationUri}"); - actualLocationChange.IsNavigationIntercepted.ShouldBeFalse(); + // assert + actualLocationChange.Location.ShouldBe($"{sut.BaseUri}{navigationUri}"); + actualLocationChange.IsNavigationIntercepted.ShouldBeFalse(); - // test helpers - void Sut_LocationChanged(object? sender, LocationChangedEventArgs e) - { - actualLocationChange = e; - } + // test helpers + void Sut_LocationChanged(object? sender, LocationChangedEventArgs e) + { + actualLocationChange = e; } + } - [Fact(DisplayName = "LocationChanged is raised on the test renderer's dispatcher")] - public void Test006() - { - var sut = CreateFakeNavigationManager(); - var cut = RenderComponent(); + [Fact(DisplayName = "LocationChanged is raised on the test renderer's dispatcher")] + public void Test006() + { + var sut = CreateFakeNavigationManager(); + var cut = RenderComponent(); - sut.NavigateTo("foo"); + sut.NavigateTo("foo"); - cut.Find("p").MarkupMatches($"

{sut.BaseUri}foo

"); - } + cut.Find("p").MarkupMatches($"

{sut.BaseUri}foo

"); + } - [Fact(DisplayName = "Uri should not be unescaped")] - public void Test007() - { - var sut = CreateFakeNavigationManager(); + [Fact(DisplayName = "Uri should not be unescaped")] + public void Test007() + { + var sut = CreateFakeNavigationManager(); - sut.NavigateTo("/with%20whitespace"); + sut.NavigateTo("/with%20whitespace"); - sut.Uri.ShouldEndWith("with%20whitespace"); - } + sut.Uri.ShouldEndWith("with%20whitespace"); + } #if !NET6_0_OR_GREATER [Theory(DisplayName = "NavigateTo(uri, forceLoad) is saved in history")] @@ -112,56 +115,186 @@ public void Test100(string uri, bool forceLoad) } #endif #if NET6_0_OR_GREATER - [Theory(DisplayName = "NavigateTo(uri, forceLoad, replaceHistoryEntry) is saved in history")] - [InlineData("/uri", false, false)] - [InlineData("/uri", true, false)] - [InlineData("/uri", false, true)] - public void Test200(string uri, bool forceLoad, bool replaceHistoryEntry) - { - var sut = CreateFakeNavigationManager(); + [Theory(DisplayName = "NavigateTo(uri, forceLoad, replaceHistoryEntry) is saved in history")] + [InlineData("/uri", false, false)] + [InlineData("/uri", true, false)] + [InlineData("/uri", false, true)] + public void Test200(string uri, bool forceLoad, bool replaceHistoryEntry) + { + var sut = CreateFakeNavigationManager(); + + sut.NavigateTo(uri, forceLoad, replaceHistoryEntry); + +#if NET6_0 + sut.History.ShouldHaveSingleItem() + .ShouldBeEquivalentTo(new NavigationHistory(uri, + new NavigationOptions { ForceLoad = forceLoad, ReplaceHistoryEntry = replaceHistoryEntry })); +#elif NET7_0_OR_GREATER + var navigationOptions = new NavigationOptions { ForceLoad = forceLoad, ReplaceHistoryEntry = + replaceHistoryEntry }; + sut.History.ShouldHaveSingleItem() + .ShouldBeEquivalentTo(new NavigationHistory(uri, navigationOptions, NavigationState.Succeeded)); +#endif + } + + [Fact(DisplayName = "NavigateTo with replaceHistoryEntry true replaces previous history entry")] + public void Test201() + { + var sut = CreateFakeNavigationManager(); + + sut.NavigateTo("/firstUrl"); + sut.NavigateTo("/secondUrl", new NavigationOptions { ReplaceHistoryEntry = true }); + +#if NET6_0 + sut.History.ShouldHaveSingleItem() + .ShouldBeEquivalentTo(new NavigationHistory("/secondUrl", + new NavigationOptions { ReplaceHistoryEntry = true })); +#elif NET7_0_OR_GREATER + sut.History.ShouldHaveSingleItem() + .ShouldBeEquivalentTo(new NavigationHistory("/secondUrl", new NavigationOptions { ReplaceHistoryEntry = + true }, NavigationState.Succeeded)); +#endif + } +#endif - sut.NavigateTo(uri, forceLoad, replaceHistoryEntry); + [Fact(DisplayName = "Navigate to an external url should set BaseUri")] + public void Test008() + { + const string externalUri = "https://bunit.dev/docs/getting-started/index.html"; + var sut = CreateFakeNavigationManager(); - sut.History.ShouldHaveSingleItem() - .ShouldBeEquivalentTo(new NavigationHistory(uri, new NavigationOptions { ForceLoad = forceLoad, ReplaceHistoryEntry = replaceHistoryEntry })); - } + sut.NavigateTo(externalUri); - [Fact(DisplayName = "NavigateTo with replaceHistoryEntry true replaces previous history entry")] - public void Test201() - { - var sut = CreateFakeNavigationManager(); + sut.BaseUri.ShouldBe("https://bunit.dev/"); + sut.Uri.ShouldBe(externalUri); + } - sut.NavigateTo("/firstUrl"); - sut.NavigateTo("/secondUrl", new NavigationOptions { ReplaceHistoryEntry = true }); + [Fact(DisplayName = "Navigate to external url should not invoke LocationChanged event")] + public void Test009() + { + var locationChangedInvoked = false; + const string externalUri = "https://bunit.dev/docs/getting-started/index.html"; + var sut = CreateFakeNavigationManager(); + sut.LocationChanged += (s, e) => locationChangedInvoked = true; - sut.History.ShouldHaveSingleItem() - .ShouldBeEquivalentTo(new NavigationHistory("/secondUrl", new NavigationOptions { ReplaceHistoryEntry = true })); - } -#endif + sut.NavigateTo(externalUri); + + locationChangedInvoked.ShouldBeFalse(); + } + +#if NET7_0_OR_GREATER + [Fact(DisplayName = "When component provides NavigationLock, FakeNavigationManager should intercept calls")] + public void Test010() + { + var fakeNavigationManager = CreateFakeNavigationManager(); + var cut = RenderComponent(); + + cut.Find("button").Click(); + + cut.Instance.NavigationIntercepted.ShouldBeTrue(); + fakeNavigationManager.History.Single().State.ShouldBe(NavigationState.Prevented); + } + + [Fact(DisplayName = "Intercepting external url's should work")] + public void Test011() + { + var fakeNavigationManager = CreateFakeNavigationManager(); + var cut = RenderComponent(); + + cut.Find("button").Click(); + + fakeNavigationManager.History.ShouldNotBeEmpty(); + } - [Fact(DisplayName = "Navigate to an external url should set BaseUri")] - public void Test008() + [Fact(DisplayName = "Exception while intercepting is set on FakeNaviationManager")] + public void Test012() + { + var fakeNavigationManager = CreateFakeNavigationManager(); + var cut = RenderComponent(); + + cut.Find("button").Click(); + + var entry = fakeNavigationManager.History.Single(); + entry.Exception.ShouldBeOfType(); + entry.State.ShouldBe(NavigationState.Faulted); + } + + private class InterceptNavigateToCounterComponent : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) { - const string externalUri = "https://bunit.dev/docs/getting-started/index.html"; - var sut = CreateFakeNavigationManager(); + builder.OpenElement(0, "button"); + builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, + () => NavigationManager.NavigateTo("/counter") + )); + builder.AddContent(2, "Goto counter"); + builder.CloseElement(); + builder.AddMarkupContent(3, "\n\n"); + builder.OpenComponent(4); + builder.AddAttribute(5, "OnBeforeInternalNavigation", TypeCheck( + EventCallback.Factory.Create(this, + InterceptNavigation + ))); + builder.CloseComponent(); + } - sut.NavigateTo(externalUri); + public bool NavigationIntercepted { get; set; } - sut.BaseUri.ShouldBe("https://bunit.dev/"); - sut.Uri.ShouldBe(externalUri); + private void InterceptNavigation(LocationChangingContext context) + { + context.PreventNavigation(); + NavigationIntercepted = true; } - [Fact(DisplayName = "Navigate to external url should not invoke LocationChanged event")] - public void Test009() + [Inject] private NavigationManager NavigationManager { get; set; } = default!; + } + + public class GotoExternalResourceComponent : ComponentBase + { +#pragma warning disable 1998 + protected override void BuildRenderTree(RenderTreeBuilder builder) { - var locationChangedInvoked = false; - const string externalUri = "https://bunit.dev/docs/getting-started/index.html"; - var sut = CreateFakeNavigationManager(); - sut.LocationChanged += (s, e) => locationChangedInvoked = true; + builder.OpenElement(0, "button"); + builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, + () => NavigationManager.NavigateTo("https://bunit.dev") + )); + builder.AddContent(2, "bunit"); + builder.CloseElement(); + builder.AddMarkupContent(3, "\n"); + builder.OpenComponent(4); + builder.AddAttribute(5, "ConfirmExternalNavigation", TypeCheck(true)); + builder.CloseComponent(); + } - sut.NavigateTo(externalUri); + [Inject] private NavigationManager NavigationManager { get; set; } = default!; + } - locationChangedInvoked.ShouldBeFalse(); + private class ThrowsExceptionInInterceptNavigationComponent : ComponentBase + { + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "button"); + builder.AddAttribute(1, "onclick", EventCallback.Factory.Create(this, + () => NavigationManager.NavigateTo("/counter") + )); + builder.AddContent(2, "Goto counter"); + builder.CloseElement(); + builder.AddMarkupContent(3, "\n\n"); + builder.OpenComponent(4); + builder.AddAttribute(5, "OnBeforeInternalNavigation", TypeCheck( + EventCallback.Factory.Create(this, + InterceptNavigation + ))); + builder.CloseComponent(); } + + private void InterceptNavigation(LocationChangingContext context) + { + throw new NotSupportedException("Don't intercept"); + } + + [Inject] private NavigationManager NavigationManager { get; set; } = default!; } +#endif } +