Skip to content

Commit 8f99afa

Browse files
committed
feat: Support supplying parameters as query string
fixes #1368
1 parent c75337d commit 8f99afa

File tree

15 files changed

+200
-57
lines changed

15 files changed

+200
-57
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ All notable changes to **bUnit** will be documented in this file. The project ad
99
### Added
1010
- Support for `IKeyedServiceProvider` in net8.0. Reported by [@ViRuSTriNiTy](https://github.com/ViRuSTriNiTy). By [@linkdotnet](https://github.com/linkdotnet).
1111

12+
### Fixed
13+
- Support for `SupplyFromQueryParameter` in net8.0. Reported by [@aayjaychan](https://github.com/aayjaychan). Fixed by [@egil](https://github.com/egil) and [@linkdotnet](https://github.com/linkdotnet).
14+
1215
## [1.26.64] - 2023-12-20
1316

1417
### Changed

src/bunit.core/ComponentParameterCollectionBuilder.cs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
using System.Linq.Expressions;
22
using System.Reflection;
3+
using System.Runtime.CompilerServices;
34
using Bunit.Extensions;
5+
using Bunit.Rendering;
46

57
namespace Bunit;
68

@@ -48,7 +50,11 @@ public ComponentParameterCollectionBuilder(Action<ComponentParameterCollectionBu
4850
/// <returns>This <see cref="ComponentParameterCollectionBuilder{TComponent}"/>.</returns>
4951
public ComponentParameterCollectionBuilder<TComponent> Add<TValue>(Expression<Func<TComponent, TValue>> parameterSelector, [AllowNull] TValue value)
5052
{
53+
#if !NET8_0_OR_GREATER
5154
var (name, cascadingValueName, isCascading) = GetParameterInfo(parameterSelector);
55+
#else
56+
var (name, cascadingValueName, isCascading) = GetParameterInfo(parameterSelector, value);
57+
#endif
5258
return isCascading
5359
? AddCascadingValueParameter(cascadingValueName, value)
5460
: AddParameter<TValue>(name, value);
@@ -338,7 +344,11 @@ public ComponentParameterCollectionBuilder<TComponent> Bind<TValue>(
338344
Action<TValue> changedAction,
339345
Expression<Func<TValue>>? valueExpression = null)
340346
{
347+
#if !NET8_0_OR_GREATER
341348
var (parameterName, _, isCascading) = GetParameterInfo(parameterSelector);
349+
#else
350+
var (parameterName, _, isCascading) = GetParameterInfo(parameterSelector, initialValue);
351+
#endif
342352

343353
if (isCascading)
344354
throw new ArgumentException("Using Bind with a cascading parameter is not allowed.", parameterName);
@@ -393,7 +403,7 @@ static string TrimEnd(string source, string value)
393403
/// or a <see cref="CascadingParameterAttribute"/>.
394404
/// </summary>
395405
/// <remarks>
396-
/// This is an untyped version of the <see cref="Add{TValue}(Expression{Func{TComponent, TValue}}, TValue)"/> method. Always
406+
/// This is an untyped version of this method named <see cref="AddUnmatched"/>. Always
397407
/// prefer the strongly typed <c>Add</c> methods whenever possible.
398408
/// </remarks>
399409
/// <typeparam name="TValue">Value type.</typeparam>
@@ -426,7 +436,12 @@ public bool TryAdd<TValue>(string name, [AllowNull] TValue value)
426436
/// <returns>The created <see cref="ComponentParameterCollection"/>.</returns>
427437
public ComponentParameterCollection Build() => parameters;
428438

429-
private static (string Name, string? CascadingValueName, bool IsCascading) GetParameterInfo<TValue>(Expression<Func<TComponent, TValue>> parameterSelector)
439+
private static (string Name, string? CascadingValueName, bool IsCascading) GetParameterInfo<TValue>(
440+
Expression<Func<TComponent, TValue>> parameterSelector
441+
#if NET8_0_OR_GREATER
442+
, object? value
443+
#endif
444+
)
430445
{
431446
if (parameterSelector is null)
432447
throw new ArgumentNullException(nameof(parameterSelector));
@@ -439,12 +454,47 @@ private static (string Name, string? CascadingValueName, bool IsCascading) GetPa
439454
: propInfoCandidate;
440455

441456
var paramAttr = propertyInfo?.GetCustomAttribute<ParameterAttribute>(inherit: true);
457+
#if !NET8_0_OR_GREATER
442458
var cascadingParamAttr = propertyInfo?.GetCustomAttribute<CascadingParameterAttribute>(inherit: true);
443459

444460
if (propertyInfo is null || (paramAttr is null && cascadingParamAttr is null))
445461
throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}' with a [Parameter] or [CascadingParameter] attribute.", nameof(parameterSelector));
446462

447463
return (propertyInfo.Name, CascadingValueName: cascadingParamAttr?.Name, IsCascading: cascadingParamAttr is not null);
464+
#else
465+
var cascadingParamAttrBase = propertyInfo?.GetCustomAttribute<CascadingParameterAttributeBase>(inherit: true);
466+
467+
if (propertyInfo is null || (paramAttr is null && cascadingParamAttrBase is null))
468+
throw new ArgumentException($"The parameter selector '{parameterSelector}' does not resolve to a public property on the component '{typeof(TComponent)}' with a [Parameter] or [CascadingParameter]attribute.", nameof(parameterSelector));
469+
470+
if (cascadingParamAttrBase is null)
471+
return (propertyInfo.Name, CascadingValueName: null, IsCascading: false);
472+
473+
var name = cascadingParamAttrBase switch
474+
{
475+
CascadingParameterAttribute cpa => cpa.Name,
476+
SupplyParameterFromQueryAttribute s => throw CreateErrorMessageForSupplyFromQuery(value, propertyInfo, s.Name),
477+
_ => throw new NotSupportedException($"The type '{cascadingParamAttrBase.GetType()}' is not supported"),
478+
};
479+
480+
return (propertyInfo.Name, CascadingValueName: name, IsCascading: true);
481+
482+
static ArgumentException CreateErrorMessageForSupplyFromQuery(
483+
object? value,
484+
MemberInfo propertyInfo,
485+
string? name)
486+
{
487+
var cascadingParameterName = name ?? propertyInfo.Name;
488+
489+
return new ArgumentException($"""
490+
To pass a value to a SupplyParameterFromQuery parameter, use the NavigationManager and navigate to the URI.
491+
For example:
492+
493+
var uri = NavigationManager.GetUriWithQueryParameter("{cascadingParameterName}", "{value}");
494+
NavigationManager.NavigateTo(uri);
495+
""");
496+
}
497+
#endif
448498
}
449499

450500
private static bool HasChildContentParameter()

src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
2929
if (renderedComponent is null)
3030
throw new ArgumentNullException(nameof(renderedComponent));
3131

32-
var renderer = renderedComponent.Services.GetRequiredService<TestRenderer>();
32+
var renderer = (TestRenderer)renderedComponent.Services.GetRequiredService<TestContextBase>().Renderer;
3333

3434
try
3535
{

src/bunit.core/Extensions/RenderedFragmentInvokeAsyncExtensions.cs

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ public static Task InvokeAsync(this IRenderedFragmentBase renderedFragment, Acti
1818
if (renderedFragment is null)
1919
throw new ArgumentNullException(nameof(renderedFragment));
2020

21-
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
22-
.Dispatcher.InvokeAsync(workItem);
21+
return renderedFragment
22+
.Services
23+
.GetRequiredService<TestContextBase>()
24+
.Renderer
25+
.Dispatcher
26+
.InvokeAsync(workItem);
2327
}
2428

2529
/// <summary>
@@ -33,8 +37,12 @@ public static Task InvokeAsync(this IRenderedFragmentBase renderedFragment, Func
3337
if (renderedFragment is null)
3438
throw new ArgumentNullException(nameof(renderedFragment));
3539

36-
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
37-
.Dispatcher.InvokeAsync(workItem);
40+
return renderedFragment
41+
.Services
42+
.GetRequiredService<TestContextBase>()
43+
.Renderer
44+
.Dispatcher
45+
.InvokeAsync(workItem);
3846
}
3947

4048
/// <summary>
@@ -48,8 +56,12 @@ public static Task<T> InvokeAsync<T>(this IRenderedFragmentBase renderedFragment
4856
if (renderedFragment is null)
4957
throw new ArgumentNullException(nameof(renderedFragment));
5058

51-
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
52-
.Dispatcher.InvokeAsync(workItem);
59+
return renderedFragment
60+
.Services
61+
.GetRequiredService<TestContextBase>()
62+
.Renderer
63+
.Dispatcher
64+
.InvokeAsync(workItem);
5365
}
5466

5567
/// <summary>
@@ -63,7 +75,11 @@ public static Task<T> InvokeAsync<T>(this IRenderedFragmentBase renderedFragment
6375
if (renderedFragment is null)
6476
throw new ArgumentNullException(nameof(renderedFragment));
6577

66-
return renderedFragment.Services.GetRequiredService<ITestRenderer>()
67-
.Dispatcher.InvokeAsync(workItem);
78+
return renderedFragment
79+
.Services
80+
.GetRequiredService<TestContextBase>()
81+
.Renderer
82+
.Dispatcher
83+
.InvokeAsync(workItem);
6884
}
6985
}

src/bunit.core/Extensions/WaitForHelpers/WaitForHelper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,8 @@ protected WaitForHelper(
5656
logger = renderedFragment.Services.CreateLogger<WaitForHelper<T>>();
5757
renderer = (TestRenderer)renderedFragment
5858
.Services
59-
.GetRequiredService<ITestRenderer>();
59+
.GetRequiredService<TestContextBase>()
60+
.Renderer;
6061
checkPassedCompletionSource = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
6162
timer = new Timer(_ =>
6263
{

src/bunit.core/TestContextBase.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@ public abstract class TestContextBase : IDisposable
1919
/// <summary>
2020
/// Gets the renderer used by the test context.
2121
/// </summary>
22-
public ITestRenderer Renderer => testRenderer ??= Services.GetRequiredService<ITestRenderer>();
22+
public ITestRenderer Renderer => testRenderer ??= CreateTestRenderer();
23+
24+
/// <summary>
25+
/// Hey YouTube, I'm a comment!
26+
/// </summary>
27+
/// <returns></returns>
28+
protected abstract ITestRenderer CreateTestRenderer();
2329

2430
/// <summary>
2531
/// Gets the service collection and service provider that is used when a

src/bunit.core/TestDoubles/PersistentComponentState/FakePersistentComponentState.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Diagnostics.CodeAnalysis;
44
using System.Text.Json;
55
using System.Threading.Tasks;
6+
using Bunit.Rendering;
67
using Microsoft.AspNetCore.Components;
78
using Microsoft.AspNetCore.Components.Infrastructure;
89
using Microsoft.AspNetCore.Components.RenderTree;
@@ -23,7 +24,7 @@ public sealed class FakePersistentComponentState
2324
};
2425
private readonly FakePersistentComponentStateStore store;
2526
private readonly Lazy<ComponentStatePersistenceManager> manager;
26-
private readonly Lazy<Renderer> renderer;
27+
private readonly Lazy<ITestRenderer> renderer;
2728

2829
/// <summary>
2930
/// Initializes a new instance of the <see cref="FakePersistentComponentState"/> class.
@@ -33,7 +34,7 @@ internal FakePersistentComponentState(IServiceProvider services)
3334
{
3435
store = new FakePersistentComponentStateStore();
3536
manager = new Lazy<ComponentStatePersistenceManager>(() => services.GetRequiredService<ComponentStatePersistenceManager>());
36-
renderer = new Lazy<Renderer>(() => services.GetRequiredService<Renderer>());
37+
renderer = new Lazy<ITestRenderer>(() => services.GetRequiredService<TestContextBase>().Renderer);
3738
}
3839

3940
/// <summary>
@@ -48,7 +49,7 @@ internal FakePersistentComponentState(IServiceProvider services)
4849
/// </remarks>
4950
public void TriggerOnPersisting()
5051
{
51-
manager.Value.PersistStateAsync(store, renderer.Value);
52+
manager.Value.PersistStateAsync(store, (Renderer)renderer.Value);
5253
manager.Value.RestoreStateAsync(store);
5354
}
5455

src/bunit.web/Extensions/RenderedFragmentExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ public static IRenderedComponent<TComponent> FindComponent<TComponent>(this IRen
5858
if (renderedFragment is null)
5959
throw new ArgumentNullException(nameof(renderedFragment));
6060

61-
var renderer = renderedFragment.Services.GetRequiredService<ITestRenderer>();
61+
var renderer = renderedFragment.Services.GetRequiredService<TestContextBase>().Renderer;
6262
return (IRenderedComponent<TComponent>)renderer.FindComponent<TComponent>(renderedFragment);
6363
}
6464

@@ -74,7 +74,7 @@ public static IReadOnlyList<IRenderedComponent<TComponent>> FindComponents<TComp
7474
if (renderedFragment is null)
7575
throw new ArgumentNullException(nameof(renderedFragment));
7676

77-
var renderer = renderedFragment.Services.GetRequiredService<ITestRenderer>();
77+
var renderer = renderedFragment.Services.GetRequiredService<TestContextBase>().Renderer;
7878
var components = renderer.FindComponents<TComponent>(renderedFragment);
7979

8080
return components.OfType<IRenderedComponent<TComponent>>().ToArray();

src/bunit.web/Extensions/TestServiceProviderExtensions.cs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
3838

3939
// bUnits fake Navigation Manager
4040
services.AddSingleton<FakeNavigationManager>();
41-
services.AddSingleton<NavigationManager>(s => s.GetRequiredService<FakeNavigationManager>());
41+
services.AddScoped<NavigationManager>(s => s.GetRequiredService<FakeNavigationManager>());
4242
services.AddSingleton<INavigationInterception, FakeNavigationInterception>();
4343

4444
// bUnits fake WebAssemblyHostEnvironment
@@ -48,14 +48,11 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
4848
#if NET8_0_OR_GREATER
4949
// bUnits fake ScrollToLocationHash
5050
services.AddSingleton<IScrollToLocationHash, BunitScrollToLocationHash>();
51+
services.AddSupplyValueFromQueryProvider();
5152
#endif
52-
53+
5354
// bUnit specific services
54-
services.AddSingleton<TestContextBase>(testContext);
55-
services.AddSingleton<WebTestRenderer>();
56-
services.AddSingleton<TestRenderer>(s => s.GetRequiredService<WebTestRenderer>());
57-
services.AddSingleton<Renderer>(s => s.GetRequiredService<WebTestRenderer>());
58-
services.AddSingleton<ITestRenderer>(s => s.GetRequiredService<WebTestRenderer>());
55+
services.AddSingleton(testContext);
5956
services.AddSingleton<HtmlComparer>();
6057
services.AddSingleton<BunitHtmlParser>();
6158
services.AddSingleton<IRenderedComponentActivator, RenderedComponentActivator>();

src/bunit.web/Rendering/BunitHtmlParser.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,13 @@ public BunitHtmlParser()
3232

3333
/// <summary>
3434
/// Initializes a new instance of the <see cref="BunitHtmlParser"/> class
35-
/// with a AngleSharp context that includes a the <paramref name="testRenderer"/> registered.
35+
/// with a AngleSharp context registered.
3636
/// </summary>
37-
public BunitHtmlParser(ITestRenderer testRenderer, HtmlComparer htmlComparer, TestContextBase testContext)
37+
public BunitHtmlParser(HtmlComparer htmlComparer, TestContextBase testContext)
3838
: this(Configuration.Default.WithCss()
39-
.With(testRenderer ?? throw new ArgumentNullException(nameof(testRenderer)))
4039
.With(htmlComparer ?? throw new ArgumentNullException(nameof(htmlComparer)))
41-
.With(testContext ?? throw new ArgumentNullException(nameof(testContext))))
40+
.With(testContext ?? throw new ArgumentNullException(nameof(testContext)))
41+
.With(testContext.Renderer))
4242
{ }
4343

4444
private BunitHtmlParser(IConfiguration angleSharpConfiguration)

0 commit comments

Comments
 (0)