Skip to content

Commit d2159cb

Browse files
committed
feat: pass new parameters directly to rendered component
fix test name
1 parent 97677b1 commit d2159cb

9 files changed

Lines changed: 300 additions & 215 deletions

File tree

CHANGELOG.md

Lines changed: 197 additions & 193 deletions
Large diffs are not rendered by default.

src/bunit.core/ComponentParameterCollection.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ public void Add(IEnumerable<ComponentParameter> parameters)
7171
/// the parameters in the collection passed to it.
7272
/// </summary>
7373
/// <typeparam name="TComponent">Type of component to render.</typeparam>
74-
[SuppressMessage("Design", "MA0051:Method is too long", Justification = "TODO: Refactor")]
7574
public RenderFragment ToRenderFragment<TComponent>()
7675
where TComponent : IComponent
7776
{

src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Bunit.Rendering;
12
using System.Runtime.ExceptionServices;
23

34
namespace Bunit;
@@ -28,24 +29,8 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
2829
if (renderedComponent is null)
2930
throw new ArgumentNullException(nameof(renderedComponent));
3031

31-
var result = renderedComponent.InvokeAsync(() =>
32-
renderedComponent.Instance.SetParametersAsync(parameters));
33-
34-
if (result.IsFaulted && result.Exception is not null)
35-
{
36-
if (result.Exception.InnerExceptions.Count == 1)
37-
{
38-
ExceptionDispatchInfo.Capture(result.Exception.InnerExceptions[0]).Throw();
39-
}
40-
else
41-
{
42-
ExceptionDispatchInfo.Capture(result.Exception).Throw();
43-
}
44-
}
45-
else if (!result.IsCompleted)
46-
{
47-
result.GetAwaiter().GetResult();
48-
}
32+
var renderer = renderedComponent.Services.GetRequiredService<TestRenderer>();
33+
renderer.SetDirectParameters(renderedComponent, parameters);
4934
}
5035

5136
/// <summary>

src/bunit.core/Rendering/ITestRenderer.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Task DispatchEventAsync(
5252
/// <summary>
5353
/// Renders a <typeparamref name="TComponent"/> with the <paramref name="parameters"/> passed to it.
5454
/// </summary>
55-
/// <typeparam name = "TComponent" > The type of component to render.</typeparam>
55+
/// <typeparam name="TComponent">The type of component to render.</typeparam>
5656
/// <param name="parameters">The parameters to pass to the component.</param>
5757
/// <returns>A <see cref="IRenderedComponentBase{TComponent}"/> that provides access to the rendered component.</returns>
5858
IRenderedComponentBase<TComponent> RenderComponent<TComponent>(ComponentParameterCollection parameters)
@@ -77,5 +77,5 @@ IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TComponent>(IRe
7777
/// <summary>
7878
/// Disposes all components rendered by the <see cref="ITestRenderer" />.
7979
/// </summary>
80-
public void DisposeComponents();
80+
void DisposeComponents();
8181
}

src/bunit.core/Rendering/TestRenderer.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Reflection;
12
using System.Runtime.ExceptionServices;
23
using Microsoft.Extensions.Logging;
34

@@ -6,8 +7,13 @@ namespace Bunit.Rendering;
67
/// <summary>
78
/// Represents a bUnit <see cref="ITestRenderer"/> used to render Blazor components and fragments during bUnit tests.
89
/// </summary>
10+
[SuppressMessage("Major Code Smell", "S3011:Reflection should not be used to increase accessibility of classes, methods, or fields", Justification = "Blazors internal API has been stable for a while now.")]
911
public class TestRenderer : Renderer, ITestRenderer
1012
{
13+
private static readonly Type RendererType = typeof(Renderer);
14+
private static readonly FieldInfo IsBatchInProgressField = RendererType.GetField("_isBatchInProgress", BindingFlags.Instance | BindingFlags.NonPublic)!;
15+
private static readonly MethodInfo GetRequiredComponentStateMethod = RendererType.GetMethod("GetRequiredComponentState", BindingFlags.Instance | BindingFlags.NonPublic)!;
16+
1117
private readonly object renderTreeUpdateLock = new();
1218
private readonly SynchronizationContext? usersSyncContext = SynchronizationContext.Current;
1319
private readonly Dictionary<int, IRenderedFragmentBase> renderedComponents = new();
@@ -18,6 +24,14 @@ public class TestRenderer : Renderer, ITestRenderer
1824
private TaskCompletionSource<Exception> unhandledExceptionTsc = new(TaskCreationOptions.RunContinuationsAsynchronously);
1925
private Exception? capturedUnhandledException;
2026

27+
private bool IsBatchInProgress
28+
{
29+
#pragma warning disable S1144 // Unused private types or members should be removed
30+
get => (bool)(IsBatchInProgressField.GetValue(this) ?? false);
31+
#pragma warning restore S1144 // Unused private types or members should be removed
32+
set => IsBatchInProgressField.SetValue(this, value);
33+
}
34+
2135
/// <inheritdoc/>
2236
public Task<Exception> UnhandledException => unhandledExceptionTsc.Task;
2337

@@ -164,6 +178,34 @@ public void DisposeComponents()
164178
AssertNoUnhandledExceptions();
165179
}
166180

181+
/// <inheritdoc/>
182+
internal void SetDirectParameters(IRenderedFragmentBase renderedComponent, ParameterView parameters)
183+
{
184+
Dispatcher.InvokeAsync(() =>
185+
{
186+
try
187+
{
188+
IsBatchInProgress = true;
189+
190+
var componentState = GetRequiredComponentStateMethod.Invoke(this, new object[] { renderedComponent.ComponentId })!;
191+
var setDirectParametersMethod = componentState.GetType().GetMethod("SetDirectParameters", BindingFlags.Public | BindingFlags.Instance)!;
192+
setDirectParametersMethod.Invoke(componentState, new object[] { parameters });
193+
}
194+
catch (TargetInvocationException ex) when (ex.InnerException is not null)
195+
{
196+
HandleException(ex.InnerException);
197+
}
198+
finally
199+
{
200+
IsBatchInProgress = false;
201+
}
202+
203+
ProcessPendingRender();
204+
});
205+
206+
AssertNoUnhandledExceptions();
207+
}
208+
167209
/// <inheritdoc/>
168210
protected override void ProcessPendingRender()
169211
{
@@ -375,7 +417,7 @@ void FindComponentsInRenderTree(int componentId)
375417
}
376418
}
377419

378-
IRenderedComponentBase<TComponent> GetOrCreateRenderedComponent<TComponent>(RenderTreeFrameDictionary framesCollection, int componentId, TComponent component)
420+
private IRenderedComponentBase<TComponent> GetOrCreateRenderedComponent<TComponent>(RenderTreeFrameDictionary framesCollection, int componentId, TComponent component)
379421
where TComponent : IComponent
380422
{
381423
if (renderedComponents.TryGetValue(componentId, out var renderedComponent))

src/bunit.web/Extensions/TestServiceProviderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ public static IServiceCollection AddDefaultTestContextServices(this IServiceColl
4848
// bUnit specific services
4949
services.AddSingleton<TestContextBase>(testContext);
5050
services.AddSingleton<WebTestRenderer>();
51+
services.AddSingleton<TestRenderer>(s => s.GetRequiredService<WebTestRenderer>());
5152
services.AddSingleton<Renderer>(s => s.GetRequiredService<WebTestRenderer>());
5253
services.AddSingleton<ITestRenderer>(s => s.GetRequiredService<WebTestRenderer>());
5354
services.AddSingleton<HtmlComparer>();

tests/bunit.core.tests/Rendering/TestRendererTest.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,38 @@ public void Test203()
423423
Renderer.UnhandledException.Result.ShouldBeOfType<InvalidOperationException>();
424424
}
425425

426+
[Fact(DisplayName = "Multiple calls to StateHasChanged from OnParametersSet with SetParametersAndRender")]
427+
public void Test204()
428+
{
429+
var cut = RenderComponent<MultipleStateHasChangedInOnParametersSet>();
430+
cut.RenderCount.ShouldBe(1);
431+
432+
cut.SetParametersAndRender();
433+
cut.RenderCount.ShouldBe(2);
434+
}
435+
436+
[Fact(DisplayName = "Multiple calls to StateHasChanged from OnParametersSet with Render")]
437+
public void Test205()
438+
{
439+
var cut = RenderComponent<MultipleStateHasChangedInOnParametersSet>();
440+
cut.RenderCount.ShouldBe(1);
441+
442+
cut.Render();
443+
cut.RenderCount.ShouldBe(2);
444+
}
445+
446+
[Fact(DisplayName = "Multiple calls to StateHasChanged from OnParametersSet with event dispatch render trigger")]
447+
public void Test206()
448+
{
449+
var cut = RenderComponent<TriggerChildContentRerenderViaClick>();
450+
var child = cut.FindComponent<MultipleStateHasChangedInOnParametersSet>();
451+
child.RenderCount.ShouldBe(1);
452+
453+
cut.Find("button").Click();
454+
455+
child.RenderCount.ShouldBe(2);
456+
}
457+
426458
internal sealed class NoChildNoParams : ComponentBase
427459
{
428460
public const string MARKUP = "hello world";
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace Bunit.TestAssets.SampleComponents;
2+
3+
public class MultipleStateHasChangedInOnParametersSet : ComponentBase
4+
{
5+
[Parameter]
6+
public int Value { get; set; }
7+
8+
protected override void OnParametersSet()
9+
{
10+
base.OnParametersSet();
11+
StateHasChanged();
12+
StateHasChanged();
13+
StateHasChanged();
14+
}
15+
}
16+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@* TriggerChildContentRerenderViaClick.razor *@
2+
<button @onclick="() => counter++">Count = @counter</button>
3+
<MultipleStateHasChangedInOnParametersSet Value="counter" />
4+
@code {
5+
int counter;
6+
}

0 commit comments

Comments
 (0)