Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async init renders #201

Merged
merged 11 commits into from
Sep 1, 2020
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ List of new features.
### Changed
List of changes in existing functionality.

- 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.

### Deprecated
List of soon-to-be removed features.

Expand All @@ -21,6 +23,8 @@ List of now removed features.
### Fixed
List of any bug fixes.

- 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.

### Security
List of fixed security vulnerabilities.

Expand Down
1 change: 1 addition & 0 deletions bunit.sln
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{6EA09ED4-B714-4E6F-B0E1-4D987F8AE520}"
ProjectSection(SolutionItems) = preProject
tests\Directory.Build.props = tests\Directory.Build.props
tests\run-tests.ps1 = tests\run-tests.ps1
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".text", ".text", "{392FCD4E-356A-412A-A854-8EE197EA65B9}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Text;
using System.Threading.Tasks;

using Bunit.Rendering;

using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;

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

return renderedComponent.Renderer.Dispatcher.InvokeAsync(callback);
return renderedComponent.Services.GetRequiredService<ITestRenderer>()
.Dispatcher.InvokeAsync(callback);
}

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

return renderedComponent.Renderer.Dispatcher.InvokeAsync(callback);
return renderedComponent.Services.GetRequiredService<ITestRenderer>()
.Dispatcher.InvokeAsync(callback);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
/// <param name="renderedComponent">The rendered component to re-render with new parameters</param>
/// <param name="parameterBuilder">An action that receives a <see cref="ComponentParameterBuilder{TComponent}"/>.</param>
public static void SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, Action<ComponentParameterBuilder<TComponent>> parameterBuilder)
where TComponent : IComponent
where TComponent : IComponent
{
if (renderedComponent is null)
throw new ArgumentNullException(nameof(renderedComponent));
Expand All @@ -70,7 +70,7 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
private static ParameterView ToParameterView(IReadOnlyList<ComponentParameter> parameters)
{
var parameterView = ParameterView.Empty;
if (parameters.Any())
if (parameters.Count > 0)
{
var paramDict = new Dictionary<string, object>();
foreach (var param in parameters)
Expand Down
29 changes: 18 additions & 11 deletions src/bunit.core/IRenderedFragmentBase.cs
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
using System;
using System.Threading.Tasks;

using Bunit.Rendering;
using Microsoft.AspNetCore.Components;

namespace Bunit
{
/// <summary>
/// Represents a rendered fragment.
/// Represents a rendered <see cref="RenderFragment"/>.
/// </summary>
public interface IRenderedFragmentBase
public interface IRenderedFragmentBase : IDisposable
{
/// <summary>
/// Gets the id of the rendered component or fragment.
/// Gets the total number times the fragment has been through its render life-cycle.
/// </summary>
int ComponentId { get; }
int RenderCount { get; }

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

/// <summary>
/// Adds or removes an event handler that will be triggered after each render of this <see cref="IRenderedFragmentBase"/>.
/// Gets the id of the rendered component or fragment.
/// </summary>
event Action OnAfterRender;
int ComponentId { get; }

/// <summary>
/// Called by the owning <see cref="ITestRenderer"/> when it finishes a render.
/// </summary>
/// <param name="renderEvent">A <see cref="RenderEvent"/> that represents a render.</param>
void OnRender(RenderEvent renderEvent);

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

/// <summary>
/// Gets the <see cref="ITestRenderer"/> renderer that rendered the component.
/// Adds or removes an event handler that will be triggered after each render of this <see cref="IRenderedFragmentBase"/>.
/// </summary>
ITestRenderer Renderer { get; }
event Action? OnAfterRender;
}
}
2 changes: 1 addition & 1 deletion src/bunit.core/Rendering/ComponentDisposedException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Bunit.Rendering
{
/// <summary>
/// Represents an exception that is thrown when a <see cref="IRenderedFragmentBase"/>'s
/// Represents an exception that is thrown when a <see cref="Bunit.IRenderedFragmentBase"/>'s
/// properties is accessed after the underlying component has been dispsoed by the renderer.
/// </summary>
public class ComponentDisposedException : Exception
Expand Down
20 changes: 0 additions & 20 deletions src/bunit.core/Rendering/IRenderEventHandler.cs

This file was deleted.

22 changes: 0 additions & 22 deletions src/bunit.core/Rendering/IRenderEventProducer.cs

This file was deleted.

29 changes: 29 additions & 0 deletions src/bunit.core/Rendering/IRenderedComponentActivator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@

using Microsoft.AspNetCore.Components;

namespace Bunit.Rendering
{
/// <summary>
/// Represents an activator for <see cref="IRenderedFragmentBase"/> and <see cref="IRenderedComponentBase{TComponent}"/> types.
/// </summary>
public interface IRenderedComponentActivator
{
/// <summary>
/// Creates an <see cref="IRenderedFragmentBase"/> with the specified <paramref name="componentId"/>.
/// </summary>
IRenderedFragmentBase CreateRenderedFragment(int componentId);

/// <summary>
/// Creates an <see cref="IRenderedComponentBase{TComponent}"/> with the specified <paramref name="componentId"/>.
/// </summary>
IRenderedComponentBase<TComponent> CreateRenderedComponent<TComponent>(int componentId)
where TComponent : IComponent;

/// <summary>
/// Creates an <see cref="IRenderedComponentBase{TComponent}"/> with the specified <paramref name="componentId"/>,
/// <paramref name="component"/>, and <paramref name="componentFrames"/>.
/// </summary>
IRenderedComponentBase<TComponent> CreateRenderedComponent<TComponent>(int componentId, TComponent component, RenderTreeFrameCollection componentFrames)
where TComponent : IComponent;
}
}
62 changes: 24 additions & 38 deletions src/bunit.core/Rendering/ITestRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,13 @@ namespace Bunit.Rendering
/// <summary>
/// Represents a generalized Blazor renderer for testing purposes.
/// </summary>
public interface ITestRenderer : IRenderEventProducer
public interface ITestRenderer
{
/// <summary>
/// Gets the <see cref="Dispatcher"/> associated with this <see cref="ITestRenderer"/>.
/// </summary>
Dispatcher Dispatcher { get; }

///// <summary>
///// Invokes the given <paramref name="callback"/> in the context of this <see cref="ITestRenderer"/>.
///// </summary>
///// <param name="callback"></param>
///// <returns>A <see cref="Task"/> that will be completed when the action has finished executing.</returns>
//Task InvokeAsync(Action callback);

/// <summary>
/// Instantiates and renders the component of type <typeparamref name="TComponent"/>.
/// </summary>
/// <typeparam name="TComponent">Type of component to render.</typeparam>
/// <param name="parameters">Parameters to pass to the component during first render.</param>
/// <returns>The component and its assigned id.</returns>
(int ComponentId, TComponent Component) RenderComponent<TComponent>(IEnumerable<ComponentParameter> parameters) where TComponent : IComponent;

/// <summary>
/// Renders the provided <paramref name="renderFragment"/> inside a wrapper and returns
/// the wrappers component id.
/// </summary>
/// <param name="renderFragment"><see cref="Microsoft.AspNetCore.Components.RenderFragment"/> to render.</param>
/// <returns>The id of the wrapper component which the <paramref name="renderFragment"/> is rendered inside.</returns>
int RenderFragment(RenderFragment renderFragment);

/// <summary>
/// Notifies the renderer that an event has occurred.
/// </summary>
Expand All @@ -51,26 +28,35 @@ public interface ITestRenderer : IRenderEventProducer
Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo fieldInfo, EventArgs eventArgs);

/// <summary>
/// Performs a depth-first search for a <typeparamref name="TComponent"/> child component of the component with the <paramref name="parentComponentId"/>.
/// Renders the <paramref name="renderFragment"/>.
/// </summary>
/// <param name="renderFragment">The <see cref="Microsoft.AspNetCore.Components.RenderFragment"/> to render.</param>
/// <returns>A <see cref="IRenderedFragmentBase"/> that provides access to the rendered <paramref name="renderFragment"/>.</returns>
IRenderedFragmentBase RenderFragment(RenderFragment renderFragment);

/// <summary>
/// Renders a <typeparamref name="TComponent"/> with the parameters <paramref name="componentParameters"/> passed to it.
/// </summary>
/// <typeparam name="TComponent">Type of component to look for.</typeparam>
/// <param name="parentComponentId">The id of the parent component.</param>
/// <returns>The first matching child component.</returns>
(int ComponentId, TComponent Component) FindComponent<TComponent>(int parentComponentId);
/// <typeparam name = "TComponent" > The type of component to render.</typeparam>
/// <param name="componentParameters">The parameters to pass to the component.</param>
/// <returns>A <see cref="IRenderedComponentBase{TComponent}"/> that provides access to the rendered component.</returns>
IRenderedComponentBase<TComponent> RenderComponent<TComponent>(IEnumerable<ComponentParameter> componentParameters)
where TComponent : IComponent;

/// <summary>
/// Performs a depth-first search for all <typeparamref name="TComponent"/> child components of the component with the <paramref name="parentComponentId"/>.
/// Performs a depth-first search for the first <typeparamref name="TComponent"/> child component of the <paramref name="parentComponent"/>.
/// </summary>
/// <typeparam name="TComponent">Type of components to look for.</typeparam>
/// <param name="parentComponentId">The id of the parent component.</param>
/// <returns>The matching child components.</returns>
IReadOnlyList<(int ComponentId, TComponent Component)> FindComponents<TComponent>(int parentComponentId);
/// <typeparam name="TComponent">Type of component to find.</typeparam>
/// <param name="parentComponent">Parent component to search.</param>
IRenderedComponentBase<TComponent> FindComponent<TComponent>(IRenderedFragmentBase parentComponent)
where TComponent : IComponent;

/// <summary>
/// Gets the current render tree for a given component.
/// Performs a depth-first search for all <typeparamref name="TComponent"/> child components of the <paramref name="parentComponent"/>.
/// </summary>
/// <param name="componentId">The id for the component.</param>
/// <returns>The <see cref="RenderTreeBuilder"/> representing the current render tree.</returns>
ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId);
/// <typeparam name="TComponent">Type of components to find.</typeparam>
/// <param name="parentComponent">Parent component to search.</param>
IReadOnlyList<IRenderedComponentBase<TComponent>> FindComponents<TComponent>(IRenderedFragmentBase parentComponent)
where TComponent : IComponent;
}
}
Loading