Skip to content

Commit 100ea01

Browse files
linkdotnetegil
andauthored
feat: Easy way to retrieve InteractiveRequestOptions (#867)
feat: Easy way to retrieve InteractiveRequestOptions * fix: simplified test runner script * adopt some documentation * Apply suggestions from code review Co-authored-by: Egil Hansen <[email protected]>
1 parent 4c415ae commit 100ea01

File tree

5 files changed

+111
-8
lines changed

5 files changed

+111
-8
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ All notable changes to **bUnit** will be documented in this file. The project ad
66

77
## [Unreleased]
88

9+
### Added
10+
- Added the `StateFromJson` method to the `NavigationHistory` type, to make it easy to deserialize navigation state stored as JSON during a call to `NavigationManager.NavigateTo`, e.g. as seen with the new `InteractiveRequestOptions` type available in .NET 7. By [@linkdotnet](https://github.com/linkdotnet) and [@egil](https://github.com/egil).
11+
912
## [1.10.14] - 2022-09-16
1013

1114
### Added

docs/site/docs/test-doubles/fake-navigation-manager.md

+28
Original file line numberDiff line numberDiff line change
@@ -136,4 +136,32 @@ navMan.NavigateTo("/counter");
136136
var navigationHistory = navMan.History.Single();
137137
Assert.Equal(NavigationState.Faulted, navigationHistory.NavigationState);
138138
Assert.NotNull(navigationHistory.Exception);
139+
```
140+
141+
## Getting the result of `NavigationManager.NavigateToLogin`
142+
[`NavigationManager.NavigateToLogin`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.components.navigationmanager.navigateto?view=aspnetcore-7.0) is a function, which was introduced with .NET 7, which allows to login dynamically. The function can also retrieve an `InteractiveRequestOptions` object, which can hold additional parameter.
143+
144+
```csharp
145+
InteractiveRequestOptions requestOptions = new()
146+
{
147+
Interaction = InteractionType.SignIn,
148+
ReturnUrl = NavigationManager.Uri,
149+
};
150+
requestOptions.TryAddAdditionalParameter("prompt", "login");
151+
NavigationManager.NavigateToLogin("authentication/login", requestOptions);
152+
```
153+
154+
A test could look like this:
155+
```csharp
156+
using var ctx = new TestContext();
157+
var navigationManager = ctx.Services.GetRequiredService<FakeNavigationManager>();
158+
159+
ActionToTriggerTheNavigationManager();
160+
161+
// This helper method retrieves the InteractiveRequestOptions object
162+
var requestOptions = navigationManager.History.Last().StateFromJson<InteractiveRequestOptions>();
163+
Asser.NotNull(requestOptions);
164+
Assert.Equal(requestOptions.Interaction, InteractionType.SignIn);
165+
options.TryGetAdditionalParameter("prompt", out string prompt);
166+
Assert.Equal(prompt, "login");
139167
```

src/bunit.web/TestDoubles/NavigationManager/NavigationHistory.cs

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
#if NET7_0_OR_GREATER
2+
using System.Text.Json;
13
using Microsoft.AspNetCore.Components.Routing;
4+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
5+
#endif
26

37
namespace Bunit.TestDoubles;
48

@@ -85,20 +89,49 @@ public NavigationHistory(
8589
State = navigationState;
8690
Exception = exception;
8791
}
92+
93+
/// <summary>
94+
/// Deserialize the content of <see cref="Options"/>.<see cref="NavigationOptions.HistoryEntryState"/>
95+
/// into <typeparamref name="T"/> if it is not null.
96+
/// </summary>
97+
/// <typeparam name="T">The type to deserialize the content of <see cref="Options"/>.<see cref="NavigationOptions.HistoryEntryState"/> to.</typeparam>
98+
/// <param name="options">The <see cref="JsonSerializerOptions" /> used when deserializing. If not provided, <see cref="JsonSerializerOptions.Default"/> is used.</param>
99+
/// <returns>The target type of the JSON value.</returns>
100+
/// <exception cref="InvalidOperationException">When <see cref="Options"/>.<see cref="NavigationOptions.HistoryEntryState"/> is null.</exception>
101+
public T? StateFromJson<T>(JsonSerializerOptions? options = null)
102+
{
103+
if (Options.HistoryEntryState is null)
104+
{
105+
throw new InvalidOperationException($"No {nameof(Options.HistoryEntryState)} has been set.");
106+
}
107+
108+
return JsonSerializer.Deserialize<T>(
109+
Options.HistoryEntryState,
110+
options ?? JsonSerializerOptions.Default);
111+
}
88112
#endif
89113

90114
/// <inheritdoc/>
91115
#if !NET6_0_OR_GREATER
92116
public bool Equals(NavigationHistory? other)
93117
=> other is not null && string.Equals(Uri, other.Uri, StringComparison.Ordinal) && Options.Equals(other.Options);
94118
#endif
95-
#if NET6_0_OR_GREATER
119+
#if NET6_0
96120
public bool Equals(NavigationHistory? other)
97121
=> other is not null
98122
&& string.Equals(Uri, other.Uri, StringComparison.Ordinal)
99123
&& Options.ForceLoad == other.Options.ForceLoad
100124
&& Options.ReplaceHistoryEntry == other.Options.ReplaceHistoryEntry;
101125
#endif
126+
#if NET7_0_OR_GREATER
127+
public bool Equals(NavigationHistory? other)
128+
=> other is not null
129+
&& string.Equals(Uri, other.Uri, StringComparison.Ordinal)
130+
&& Options.ForceLoad == other.Options.ForceLoad
131+
&& Options.ReplaceHistoryEntry == other.Options.ReplaceHistoryEntry
132+
&& State == other.State
133+
&& Exception == other.Exception;
134+
#endif
102135

103136
/// <inheritdoc/>
104137
public override bool Equals(object? obj) => obj is NavigationHistory other && Equals(other);

tests/bunit.web.tests/TestDoubles/NavigationManager/FakeNavigationManagerTest.cs

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System.Text.Json;
2+
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
3+
14
namespace Bunit.TestDoubles;
25

36
using static Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers;
@@ -219,6 +222,47 @@ public void Test012()
219222
entry.State.ShouldBe(NavigationState.Faulted);
220223
}
221224

225+
[Fact(DisplayName = "StateFromJson deserialize InteractiveRequestOptions")]
226+
public void Test013()
227+
{
228+
var fakeNavigationManager = CreateFakeNavigationManager();
229+
var requestOptions = new InteractiveRequestOptions
230+
{
231+
ReturnUrl = "return", Interaction = InteractionType.SignIn,
232+
};
233+
requestOptions.TryAddAdditionalParameter("library", "bunit");
234+
235+
fakeNavigationManager.NavigateToLogin("/some-url", requestOptions);
236+
237+
var options = fakeNavigationManager.History.Last().StateFromJson<InteractiveRequestOptions>();
238+
options.ShouldNotBeNull();
239+
options.Interaction.ShouldBe(InteractionType.SignIn);
240+
options.ReturnUrl.ShouldBe("return");
241+
options.TryGetAdditionalParameter("library", out string libraryName).ShouldBeTrue();
242+
libraryName.ShouldBe("bunit");
243+
}
244+
245+
[Fact(DisplayName = "Given no content in state then StateFromJson throws")]
246+
public void Test014()
247+
{
248+
var fakeNavigationManager = CreateFakeNavigationManager();
249+
fakeNavigationManager.NavigateTo("/some-url");
250+
251+
Should.Throw<InvalidOperationException>(
252+
() => fakeNavigationManager.History.Last().StateFromJson<InteractiveRequestOptions>());
253+
}
254+
255+
[Fact(DisplayName = "StateFromJson with invalid json throws")]
256+
public void Test015()
257+
{
258+
var fakeNavigationManager = CreateFakeNavigationManager();
259+
260+
fakeNavigationManager.NavigateTo("/login", new NavigationOptions { HistoryEntryState = "<invalidjson>" });
261+
262+
Should.Throw<JsonException>(
263+
() => fakeNavigationManager.History.Last().StateFromJson<InteractiveRequestOptions>());
264+
}
265+
222266
private class InterceptNavigateToCounterComponent : ComponentBase
223267
{
224268
protected override void BuildRenderTree(RenderTreeBuilder builder)
@@ -251,7 +295,6 @@ private void InterceptNavigation(LocationChangingContext context)
251295

252296
public class GotoExternalResourceComponent : ComponentBase
253297
{
254-
#pragma warning disable 1998
255298
protected override void BuildRenderTree(RenderTreeBuilder builder)
256299
{
257300
builder.OpenElement(0, "button");

tests/run-tests.ps1

+2-6
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,10 @@ for ($num = 1 ; $num -le $maxRuns ; $num++)
1010

1111
if($filter)
1212
{
13-
dotnet test .\bunit.core.tests\bunit.core.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s --nologo --filter $filter --logger:"console;verbosity=normal"
14-
dotnet test .\bunit.web.tests\bunit.web.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s --nologo --filter $filter --logger:"console;verbosity=normal"
15-
dotnet test .\bunit.web.testcomponents.tests\bunit.web.testcomponents.teststests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s --nologo --filter $filter --logger:"console;verbosity=normal"
13+
dotnet test ..\bunit.sln -c $mode --no-restore --no-build --blame --nologo --filter $filter
1614
}
1715
else
1816
{
19-
dotnet test .\bunit.core.tests\bunit.core.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s --nologo
20-
dotnet test .\bunit.web.tests\bunit.web.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s --nologo
21-
dotnet test .\bunit.web.testcomponents.tests\bunit.web.testcomponents.tests.csproj -c $mode --no-restore --no-build --blame-hang --blame-hang-timeout 100s --nologo
17+
dotnet test ..\bunit.sln -c $mode --no-restore --no-build --blame --nologo
2218
}
2319
}

0 commit comments

Comments
 (0)