Skip to content

Commit 9ed7f33

Browse files
authored
Rewritting test renderer and related types (#201)
* new test renderer prototype * new test renderer prototype done * Refactor to new renderer done * Fixed order of notifications from rendered fragment * Added code comments to refactored code, simplified TestRenderer * Updated code docs, small refactors to code * Automated dotnet-format update * tests moved around * Simplified test context services registration * Tweaks to code, updates to changelog * Automated dotnet-format update
1 parent c72bee0 commit 9ed7f33

37 files changed

+1114
-566
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ List of new features.
1212
### Changed
1313
List of changes in existing functionality.
1414

15+
- Related to [#189](https://github.com/egil/bUnit/issues/189), a bunch of the core `ITestRenderer` and related types have changed. The internals of `ITestRenderer` is now less exposed and the test renderer is now in control of when rendered components and rendered fragments are created, and when they are updated. This enables the test renderer to protect against race conditions when the `FindComponent`, `FindComponents`, `RenderFragment`, and `RenderComponent` methods are called.
16+
1517
### Deprecated
1618
List of soon-to-be removed features.
1719

@@ -21,6 +23,8 @@ List of now removed features.
2123
### Fixed
2224
List of any bug fixes.
2325

26+
- Fixes [#189](https://github.com/egil/bUnit/issues/189): The test renderer did not correctly protect against a race condition during initial rendering of a component, and that could in some rare circumstances cause a test to fail when it should not. This has been addressed in this release with a major rewrite of the test renderer, which now controls and owns the rendered component and rendered fragment instances which is created when a component is rendered. By [@egil](https://github.com/egil) in [#201](https://github.com/egil/bUnit/pull/201). Credits to [@Smurf-IV](https://github.com/Smurf-IV) for reporting and helping investigate this issue.
27+
2428
### Security
2529
List of fixed security vulnerabilities.
2630

bunit.sln

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ EndProject
1919
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6EA09ED4-B714-4E6F-B0E1-4D987F8AE520}"
2020
ProjectSection(SolutionItems) = preProject
2121
tests\Directory.Build.props = tests\Directory.Build.props
22+
tests\run-tests.ps1 = tests\run-tests.ps1
2223
EndProjectSection
2324
EndProject
2425
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".text", ".text", "{392FCD4E-356A-412A-A854-8EE197EA65B9}"

src/bunit.core/Extensions/RenderedComponentInvokeAsyncExtension.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
using System;
22
using System.Text;
33
using System.Threading.Tasks;
4+
45
using Bunit.Rendering;
6+
57
using Microsoft.AspNetCore.Components;
8+
using Microsoft.Extensions.DependencyInjection;
69

710
namespace Bunit
811
{
@@ -23,7 +26,8 @@ public static Task InvokeAsync<TComponent>(this IRenderedComponentBase<TComponen
2326
if (renderedComponent is null)
2427
throw new ArgumentNullException(nameof(renderedComponent));
2528

26-
return renderedComponent.Renderer.Dispatcher.InvokeAsync(callback);
29+
return renderedComponent.Services.GetRequiredService<ITestRenderer>()
30+
.Dispatcher.InvokeAsync(callback);
2731
}
2832

2933
/// <summary>
@@ -38,7 +42,8 @@ public static Task InvokeAsync<TComponent>(this IRenderedComponentBase<TComponen
3842
if (renderedComponent is null)
3943
throw new ArgumentNullException(nameof(renderedComponent));
4044

41-
return renderedComponent.Renderer.Dispatcher.InvokeAsync(callback);
45+
return renderedComponent.Services.GetRequiredService<ITestRenderer>()
46+
.Dispatcher.InvokeAsync(callback);
4247
}
4348
}
4449
}

src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
5353
/// <param name="renderedComponent">The rendered component to re-render with new parameters</param>
5454
/// <param name="parameterBuilder">An action that receives a <see cref="ComponentParameterBuilder{TComponent}"/>.</param>
5555
public static void SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, Action<ComponentParameterBuilder<TComponent>> parameterBuilder)
56-
where TComponent : IComponent
56+
where TComponent : IComponent
5757
{
5858
if (renderedComponent is null)
5959
throw new ArgumentNullException(nameof(renderedComponent));
@@ -70,7 +70,7 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
7070
private static ParameterView ToParameterView(IReadOnlyList<ComponentParameter> parameters)
7171
{
7272
var parameterView = ParameterView.Empty;
73-
if (parameters.Any())
73+
if (parameters.Count > 0)
7474
{
7575
var paramDict = new Dictionary<string, object>();
7676
foreach (var param in parameters)
Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,44 @@
11
using System;
2-
using System.Threading.Tasks;
2+
33
using Bunit.Rendering;
4+
using Microsoft.AspNetCore.Components;
45

56
namespace Bunit
67
{
78
/// <summary>
8-
/// Represents a rendered fragment.
9+
/// Represents a rendered <see cref="RenderFragment"/>.
910
/// </summary>
10-
public interface IRenderedFragmentBase
11+
public interface IRenderedFragmentBase : IDisposable
1112
{
1213
/// <summary>
13-
/// Gets the id of the rendered component or fragment.
14+
/// Gets the total number times the fragment has been through its render life-cycle.
1415
/// </summary>
15-
int ComponentId { get; }
16+
int RenderCount { get; }
1617

1718
/// <summary>
18-
/// Gets the total number times the fragment has been through its render life-cycle.
19+
/// Gets whether the rendered component or fragment has been disposed by the <see cref="ITestRenderer"/>.
1920
/// </summary>
20-
int RenderCount { get; }
21+
bool IsDisposed { get; }
2122

2223
/// <summary>
23-
/// Adds or removes an event handler that will be triggered after each render of this <see cref="IRenderedFragmentBase"/>.
24+
/// Gets the id of the rendered component or fragment.
2425
/// </summary>
25-
event Action OnAfterRender;
26+
int ComponentId { get; }
27+
28+
/// <summary>
29+
/// Called by the owning <see cref="ITestRenderer"/> when it finishes a render.
30+
/// </summary>
31+
/// <param name="renderEvent">A <see cref="RenderEvent"/> that represents a render.</param>
32+
void OnRender(RenderEvent renderEvent);
2633

2734
/// <summary>
2835
/// Gets the <see cref="IServiceProvider"/> used when rendering the component.
2936
/// </summary>
3037
IServiceProvider Services { get; }
3138

3239
/// <summary>
33-
/// Gets the <see cref="ITestRenderer"/> renderer that rendered the component.
40+
/// Adds or removes an event handler that will be triggered after each render of this <see cref="IRenderedFragmentBase"/>.
3441
/// </summary>
35-
ITestRenderer Renderer { get; }
42+
event Action? OnAfterRender;
3643
}
3744
}

src/bunit.core/Rendering/ComponentDisposedException.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace Bunit.Rendering
66
{
77
/// <summary>
8-
/// Represents an exception that is thrown when a <see cref="IRenderedFragmentBase"/>'s
8+
/// Represents an exception that is thrown when a <see cref="Bunit.IRenderedFragmentBase"/>'s
99
/// properties is accessed after the underlying component has been dispsoed by the renderer.
1010
/// </summary>
1111
public class ComponentDisposedException : Exception

src/bunit.core/Rendering/IRenderEventHandler.cs

Lines changed: 0 additions & 20 deletions
This file was deleted.

src/bunit.core/Rendering/IRenderEventProducer.cs

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
using Microsoft.AspNetCore.Components;
3+
4+
namespace Bunit.Rendering
5+
{
6+
/// <summary>
7+
/// Represents an activator for <see cref="IRenderedFragmentBase"/> and <see cref="IRenderedComponentBase{TComponent}"/> types.
8+
/// </summary>
9+
public interface IRenderedComponentActivator
10+
{
11+
/// <summary>
12+
/// Creates an <see cref="IRenderedFragmentBase"/> with the specified <paramref name="componentId"/>.
13+
/// </summary>
14+
IRenderedFragmentBase CreateRenderedFragment(int componentId);
15+
16+
/// <summary>
17+
/// Creates an <see cref="IRenderedComponentBase{TComponent}"/> with the specified <paramref name="componentId"/>.
18+
/// </summary>
19+
IRenderedComponentBase<TComponent> CreateRenderedComponent<TComponent>(int componentId)
20+
where TComponent : IComponent;
21+
22+
/// <summary>
23+
/// Creates an <see cref="IRenderedComponentBase{TComponent}"/> with the specified <paramref name="componentId"/>,
24+
/// <paramref name="component"/>, and <paramref name="componentFrames"/>.
25+
/// </summary>
26+
IRenderedComponentBase<TComponent> CreateRenderedComponent<TComponent>(int componentId, TComponent component, RenderTreeFrameCollection componentFrames)
27+
where TComponent : IComponent;
28+
}
29+
}

src/bunit.core/Rendering/ITestRenderer.cs

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,36 +11,13 @@ namespace Bunit.Rendering
1111
/// <summary>
1212
/// Represents a generalized Blazor renderer for testing purposes.
1313
/// </summary>
14-
public interface ITestRenderer : IRenderEventProducer
14+
public interface ITestRenderer
1515
{
1616
/// <summary>
1717
/// Gets the <see cref="Dispatcher"/> associated with this <see cref="ITestRenderer"/>.
1818
/// </summary>
1919
Dispatcher Dispatcher { get; }
2020

21-
///// <summary>
22-
///// Invokes the given <paramref name="callback"/> in the context of this <see cref="ITestRenderer"/>.
23-
///// </summary>
24-
///// <param name="callback"></param>
25-
///// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
26-
//Task InvokeAsync(Action callback);
27-
28-
/// <summary>
29-
/// Instantiates and renders the component of type <typeparamref name="TComponent"/>.
30-
/// </summary>
31-
/// <typeparam name="TComponent">Type of component to render.</typeparam>
32-
/// <param name="parameters">Parameters to pass to the component during first render.</param>
33-
/// <returns>The component and its assigned id.</returns>
34-
(int ComponentId, TComponent Component) RenderComponent<TComponent>(IEnumerable<ComponentParameter> parameters) where TComponent : IComponent;
35-
36-
/// <summary>
37-
/// Renders the provided <paramref name="renderFragment"/> inside a wrapper and returns
38-
/// the wrappers component id.
39-
/// </summary>
40-
/// <param name="renderFragment"><see cref="Microsoft.AspNetCore.Components.RenderFragment"/> to render.</param>
41-
/// <returns>The id of the wrapper component which the <paramref name="renderFragment"/> is rendered inside.</returns>
42-
int RenderFragment(RenderFragment renderFragment);
43-
4421
/// <summary>
4522
/// Notifies the renderer that an event has occurred.
4623
/// </summary>
@@ -51,26 +28,35 @@ public interface ITestRenderer : IRenderEventProducer
5128
Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs);
5229

5330
/// <summary>
54-
/// Performs a depth-first search for a <typeparamref name="TComponent"/> child component of the component with the <paramref name="parentComponentId"/>.
31+
/// Renders the <paramref name="renderFragment"/>.
32+
/// </summary>
33+
/// <param name="renderFragment">The <see cref="Microsoft.AspNetCore.Components.RenderFragment"/> to render.</param>
34+
/// <returns>A <see cref="IRenderedFragmentBase"/> that provides access to the rendered <paramref name="renderFragment"/>.</returns>
35+
IRenderedFragmentBase RenderFragment(RenderFragment renderFragment);
36+
37+
/// <summary>
38+
/// Renders a <typeparamref name="TComponent"/> with the parameters <paramref name="componentParameters"/> passed to it.
5539
/// </summary>
56-
/// <typeparam name="TComponent">Type of component to look for.</typeparam>
57-
/// <param name="parentComponentId">The id of the parent component.</param>
58-
/// <returns>The first matching child component.</returns>
59-
(int ComponentId, TComponent Component) FindComponent<TComponent>(int parentComponentId);
40+
/// <typeparam name = "TComponent" > The type of component to render.</typeparam>
41+
/// <param name="componentParameters">The parameters to pass to the component.</param>
42+
/// <returns>A <see cref="IRenderedComponentBase{TComponent}"/> that provides access to the rendered component.</returns>
43+
IRenderedComponentBase<TComponent> RenderComponent<TComponent>(IEnumerable<ComponentParameter> componentParameters)
44+
where TComponent : IComponent;
6045

6146
/// <summary>
62-
/// Performs a depth-first search for all <typeparamref name="TComponent"/> child components of the component with the <paramref name="parentComponentId"/>.
47+
/// Performs a depth-first search for the first <typeparamref name="TComponent"/> child component of the <paramref name="parentComponent"/>.
6348
/// </summary>
64-
/// <typeparam name="TComponent">Type of components to look for.</typeparam>
65-
/// <param name="parentComponentId">The id of the parent component.</param>
66-
/// <returns>The matching child components.</returns>
67-
IReadOnlyList<(int ComponentId, TComponent Component)> FindComponents<TComponent>(int parentComponentId);
49+
/// <typeparam name="TComponent">Type of component to find.</typeparam>
50+
/// <param name="parentComponent">Parent component to search.</param>
51+
IRenderedComponentBase<TComponent> FindComponent<TComponent>(IRenderedFragmentBase parentComponent)
52+
where TComponent : IComponent;
6853

6954
/// <summary>
70-
/// Gets the current render tree for a given component.
55+
/// Performs a depth-first search for all <typeparamref name="TComponent"/> child components of the <paramref name="parentComponent"/>.
7156
/// </summary>
72-
/// <param name="componentId">The id for the component.</param>
73-
/// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
74-
ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId);
57+
/// <typeparam name="TComponent">Type of components to find.</typeparam>
58+
/// <param name="parentComponent">Parent component to search.</param>
59+
IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TComponent>(IRenderedFragmentBase parentComponent)
60+
where TComponent : IComponent;
7561
}
7662
}

0 commit comments

Comments
 (0)