Skip to content

Move to an async first API #770

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

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -504,3 +504,5 @@ dotnet_diagnostic.S125.severity = none # S125: Sections of code should not be co
dotnet_diagnostic.S3459.severity = none # S3459: Unassigned members should be removed
dotnet_diagnostic.S3871.severity = none # S3871: Exception types should be "public"
dotnet_diagnostic.S1186.severity = none # S1186: Methods should not be empty

dotnet_diagnostic.S4457.severity = none
18 changes: 9 additions & 9 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ List of added functionality in this release.

```csharp
// <App /> component uses <FocusOnNavigate>
var cut = RenderComponent<App>();
var cut = await RenderComponent<App>();

// Verifies that <FocusOnNavigate> called it's JavaScript function
var invocation = JSInterop.VerifyFocusOnNavigateInvoke();
Expand Down Expand Up @@ -455,7 +455,7 @@ List of new features.
- Added support for components that call `ElementReference.FocusAsync`. These calls are handled by the bUnits JSInterop, that also allows you to verify that `FocusAsync` has been called for a specific element. For example, if a component has rendered an `<input>` element, then the following code will verify that it has been focused using `FocusAsync`:

```csharp
var cut = RenderComponent<FocusingComponent>();
var cut = await RenderComponent<FocusingComponent>();

var input = cut.Find("input");

Expand Down Expand Up @@ -854,7 +854,7 @@ This has changed with the _bUnit.xUnit_ library, that now includes a way for it
If you prefer writing your tests in C# only, you will be happy to know that there is now a new strongly typed way to pass parameters to components, using a builder. E.g., to render a `ContactInfo` component:

```c#
var cut = RenderComponent<ContactInfo>(parameters => parameters
var cut = await RenderComponent<ContactInfo>(parameters => parameters
.Add(p => p.Name, "Egil Hansen")
.Add(p => p.Country, "Iceland")
);
Expand Down Expand Up @@ -976,7 +976,7 @@ The latest version of the library is availble on NuGet:
public void WaitForStateExample()
{
// Arrange
var cut = RenderComponent<DelayedRenderOnClick>();
var cut = await RenderComponent<DelayedRenderOnClick>();

// Act
cut.Find("button").Click();
Expand All @@ -999,7 +999,7 @@ The latest version of the library is availble on NuGet:
public void WaitForAssertionExample()
{
// Arrange
var cut = RenderComponent<DelayedRenderOnClick>();
var cut = await RenderComponent<DelayedRenderOnClick>();

// Act
cut.Find("button").Click();
Expand Down Expand Up @@ -1071,7 +1071,7 @@ The latest version of the library is availble on NuGet:
And the test code:

```csharp
var cut = RenderComponent<SimpleWithTemplate<int>>(
var cut = await RenderComponent<SimpleWithTemplate<int>>(
("Data", new int[] { 1, 2 }),
Template<int>("Template", num => $"<p>{num}</p>")
);
Expand All @@ -1082,7 +1082,7 @@ The latest version of the library is availble on NuGet:
Using the more general `Template` helper methods, you need to write the `RenderTreeBuilder` logic yourself, e.g.:

```csharp
var cut = RenderComponent<SimpleWithTemplate<int>>(
var cut = await RenderComponent<SimpleWithTemplate<int>>(
("Data", new int[] { 1, 2 }),
Template<int>("Template", num => builder => builder.AddMarkupContent(0, $"<p>{num}</p>"))
);
Expand Down Expand Up @@ -1130,7 +1130,7 @@ The latest version of the library is availble on NuGet:
```csharp
public void AutoRefreshQueriesForNewElementsAutomatically()
{
var cut = RenderComponent<ClickAddsLi>();
var cut = await RenderComponent<ClickAddsLi>();
var liElements = cut.FindAll("li", enableAutoRefresh: true);
liElements.Count.ShouldBe(0);

Expand All @@ -1145,7 +1145,7 @@ The latest version of the library is availble on NuGet:
```csharp
public void RefreshQueriesForNewElements()
{
var cut = RenderComponent<ClickAddsLi>();
var cut = await RenderComponent<ClickAddsLi>();
var liElements = cut.FindAll("li");
liElements.Count.ShouldBe(0);

Expand Down
10 changes: 3 additions & 7 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
<DotNet5Version>5.0.0</DotNet5Version>
<DotNet6Version>6.0.0</DotNet6Version>
<DotNet7Version>7.0.0-*</DotNet7Version>
<RunAnalyzers >false</RunAnalyzers>
<RunAnalyzersDuringBuild >false</RunAnalyzersDuringBuild>
<RunAnalyzersDuringLiveAnalysis>false</RunAnalyzersDuringLiveAnalysis>
</PropertyGroup>

<!-- Solution wide properties -->
Expand Down Expand Up @@ -45,13 +48,6 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<!-- Shared code analyzers used for all projects in the solution -->
<ItemGroup Label="Code Analyzers">
<PackageReference Include="AsyncFixer" Version="1.6.0" PrivateAssets="All" />
<PackageReference Include="Meziantou.Analyzer" Version="1.0.702" PrivateAssets="All" />
<PackageReference Include="SonarAnalyzer.CSharp" Version="8.40.0.48530" PrivateAssets="All" />
</ItemGroup>

<ItemGroup Label="Implicit usings"
Condition="$(MSBuildProjectName) != 'bunit.template' AND $(MSBuildProjectName) != 'bunit' AND $(MSBuildProjectName) != 'AngleSharpWrappers.Tests'">
<Using Include="Microsoft.AspNetCore.Components" />
Expand Down
4 changes: 2 additions & 2 deletions benchmark/bunit.benchmarks/Benchmark.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ namespace Bunit;
public class Benchmark : BenchmarkBase
{
[Benchmark]
public IRenderedComponentBase<Counter> RenderCounter()
public Task<IRenderedComponentBase<Counter>> RenderCounter()
{
return RenderComponent<Counter>();
}
}
}
8 changes: 4 additions & 4 deletions benchmark/bunit.benchmarks/BenchmarkBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ public abstract class BenchmarkBase
{
private static readonly ComponentParameterCollection EmptyParameter = new();
private readonly ServiceCollection services = new();

protected TestRenderer Renderer { get; private set; } = default!;

[GlobalSetup]
public void Setup()
{
RegisterServices(services);

var serviceProvider = services.BuildServiceProvider();
Renderer = new TestRenderer(
new RenderedComponentActivator(serviceProvider),
Expand All @@ -32,7 +32,7 @@ public void Cleanup()
Renderer.Dispose();
}

protected IRenderedComponentBase<TComponent> RenderComponent<TComponent>()
protected Task<IRenderedComponentBase<TComponent>> RenderComponent<TComponent>()
where TComponent : IComponent =>
Renderer.RenderComponent<TComponent>(EmptyParameter);

Expand All @@ -44,4 +44,4 @@ protected virtual void RegisterServices(IServiceCollection serviceCollection)
{
services.AddSingleton<BunitHtmlParser>();
}
}
}
32 changes: 7 additions & 25 deletions src/bunit.core/Extensions/RenderedComponentRenderExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using System.Runtime.ExceptionServices;

namespace Bunit;

/// <summary>
Expand All @@ -12,7 +10,7 @@ public static class RenderedComponentRenderExtensions
/// </summary>
/// <param name="renderedComponent">The rendered component to re-render.</param>
/// <typeparam name="TComponent">The type of the component.</typeparam>
public static void Render<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent)
public static Task Render<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent)
where TComponent : IComponent
=> SetParametersAndRender(renderedComponent, ParameterView.Empty);

Expand All @@ -22,30 +20,14 @@ public static void Render<TComponent>(this IRenderedComponentBase<TComponent> re
/// <param name="renderedComponent">The rendered component to re-render with new parameters.</param>
/// <param name="parameters">Parameters to pass to the component upon rendered.</param>
/// <typeparam name="TComponent">The type of the component.</typeparam>
public static void SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, ParameterView parameters)
public static Task SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, ParameterView parameters)
where TComponent : IComponent
{
if (renderedComponent is null)
throw new ArgumentNullException(nameof(renderedComponent));

var result = renderedComponent.InvokeAsync(() =>
return renderedComponent.InvokeAsync(() =>
renderedComponent.Instance.SetParametersAsync(parameters));

if (result.IsFaulted && result.Exception is not null)
{
if (result.Exception.InnerExceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(result.Exception.InnerExceptions[0]).Throw();
}
else
{
ExceptionDispatchInfo.Capture(result.Exception).Throw();
}
}
else if (!result.IsCompleted)
{
result.GetAwaiter().GetResult();
}
}

/// <summary>
Expand All @@ -54,15 +36,15 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
/// <param name="renderedComponent">The rendered component to re-render with new parameters.</param>
/// <param name="parameters">Parameters to pass to the component upon rendered.</param>
/// <typeparam name="TComponent">The type of the component.</typeparam>
public static void SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, params ComponentParameter[] parameters)
public static Task SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, params ComponentParameter[] parameters)
where TComponent : IComponent
{
if (renderedComponent is null)
throw new ArgumentNullException(nameof(renderedComponent));
if (parameters is null)
throw new ArgumentNullException(nameof(parameters));

SetParametersAndRender(renderedComponent, ToParameterView(parameters));
return SetParametersAndRender(renderedComponent, ToParameterView(parameters));
}

/// <summary>
Expand All @@ -71,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="ComponentParameterCollectionBuilder{TComponent}"/>.</param>
/// <typeparam name="TComponent">The type of the component.</typeparam>
public static void SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, Action<ComponentParameterCollectionBuilder<TComponent>> parameterBuilder)
public static Task SetParametersAndRender<TComponent>(this IRenderedComponentBase<TComponent> renderedComponent, Action<ComponentParameterCollectionBuilder<TComponent>> parameterBuilder)
where TComponent : IComponent
{
if (renderedComponent is null)
Expand All @@ -80,7 +62,7 @@ public static void SetParametersAndRender<TComponent>(this IRenderedComponentBas
throw new ArgumentNullException(nameof(parameterBuilder));

var builder = new ComponentParameterCollectionBuilder<TComponent>(parameterBuilder);
SetParametersAndRender(renderedComponent, ToParameterView(builder.Build()));
return SetParametersAndRender(renderedComponent, ToParameterView(builder.Build()));
}

private static ParameterView ToParameterView(IReadOnlyCollection<ComponentParameter> parameters)
Expand Down
8 changes: 4 additions & 4 deletions src/bunit.core/Extensions/TestContextBaseRenderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ public static class TestContextBaseRenderExtensions
/// <param name="testContext">Test context to use to render with.</param>
/// <param name="renderFragment">The <see cref="RenderInsideRenderTree"/> that contains a declaration of the component.</param>
/// <returns>A <see cref="IRenderedComponentBase{TComponent}"/>.</returns>
public static IRenderedComponentBase<TComponent> RenderInsideRenderTree<TComponent>(this TestContextBase testContext, RenderFragment renderFragment)
public static async Task<IRenderedComponentBase<TComponent>> RenderInsideRenderTree<TComponent>(this TestContextBase testContext, RenderFragment renderFragment)
where TComponent : IComponent
{
if (testContext is null)
throw new ArgumentNullException(nameof(testContext));

var baseResult = RenderInsideRenderTree(testContext, renderFragment);
var baseResult = await RenderInsideRenderTree(testContext, renderFragment);
return testContext.Renderer.FindComponent<TComponent>(baseResult);
}

Expand All @@ -30,7 +30,7 @@ public static IRenderedComponentBase<TComponent> RenderInsideRenderTree<TCompone
/// <param name="testContext">Test context to use to render with.</param>
/// <param name="renderFragment">The <see cref="RenderInsideRenderTree"/> to render.</param>
/// <returns>A <see cref="IRenderedFragmentBase"/>.</returns>
public static IRenderedFragmentBase RenderInsideRenderTree(this TestContextBase testContext, RenderFragment renderFragment)
public static async Task<IRenderedFragmentBase> RenderInsideRenderTree(this TestContextBase testContext, RenderFragment renderFragment)
{
if (testContext is null)
throw new ArgumentNullException(nameof(testContext));
Expand All @@ -40,7 +40,7 @@ public static IRenderedFragmentBase RenderInsideRenderTree(this TestContextBase
// added to the test context.
var wrappedInFragmentContainer = FragmentContainer.Wrap(renderFragment);
var wrappedInRenderTree = testContext.RenderTree.Wrap(wrappedInFragmentContainer);
var resultBase = testContext.Renderer.RenderFragment(wrappedInRenderTree);
var resultBase = await testContext.Renderer.RenderFragment(wrappedInRenderTree);

return testContext.Renderer.FindComponent<FragmentContainer>(resultBase);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,11 @@ public static class RenderedFragmentWaitForHelperExtensions
/// <param name="statePredicate">The predicate to invoke after each render, which must returns <c>true</c> when the desired state has been reached.</param>
/// <param name="timeout">The maximum time to wait for the desired state.</param>
/// <exception cref="WaitForFailedException">Thrown if the <paramref name="statePredicate"/> throw an exception during invocation, or if the timeout has been reached. See the inner exception for details.</exception>
public static void WaitForState(this IRenderedFragmentBase renderedFragment, Func<bool> statePredicate, TimeSpan? timeout = null)
public static async Task WaitForState(this IRenderedFragmentBase renderedFragment, Func<bool> statePredicate, TimeSpan? timeout = null)
{
using var waiter = new WaitForStateHelper(renderedFragment, statePredicate, timeout);

try
{
waiter.WaitTask.GetAwaiter().GetResult();
}
catch (Exception e)
{
if (e is AggregateException aggregateException && aggregateException.InnerExceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
}
else
{
ExceptionDispatchInfo.Capture(e).Throw();
}
}
await waiter.WaitTask;
}

/// <summary>
Expand All @@ -52,24 +38,10 @@ public static void WaitForState(this IRenderedFragmentBase renderedFragment, Fun
/// <param name="timeout">The maximum time to attempt the verification.</param>
/// <exception cref="WaitForFailedException">Thrown if the timeout has been reached. See the inner exception to see the captured assertion exception.</exception>
[AssertionMethod]
public static void WaitForAssertion(this IRenderedFragmentBase renderedFragment, Action assertion, TimeSpan? timeout = null)
public static async Task WaitForAssertion(this IRenderedFragmentBase renderedFragment, Action assertion, TimeSpan? timeout = null)
{
using var waiter = new WaitForAssertionHelper(renderedFragment, assertion, timeout);

try
{
waiter.WaitTask.GetAwaiter().GetResult();
}
catch (Exception e)
{
if (e is AggregateException aggregateException && aggregateException.InnerExceptions.Count == 1)
{
ExceptionDispatchInfo.Capture(aggregateException.InnerExceptions[0]).Throw();
}
else
{
ExceptionDispatchInfo.Capture(e).Throw();
}
}
await waiter.WaitTask;
}
}
4 changes: 2 additions & 2 deletions src/bunit.core/Rendering/ITestRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ Task DispatchEventAsync(
/// </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);
Task<IRenderedFragmentBase> RenderFragment(RenderFragment renderFragment);

/// <summary>
/// Renders a <typeparamref name="TComponent"/> with the <paramref name="parameters"/> passed to it.
/// </summary>
/// <typeparam name = "TComponent" > The type of component to render.</typeparam>
/// <param name="parameters">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>(ComponentParameterCollection parameters)
Task<IRenderedComponentBase<TComponent>> RenderComponent<TComponent>(ComponentParameterCollection parameters)
where TComponent : IComponent;

/// <summary>
Expand Down
20 changes: 4 additions & 16 deletions src/bunit.core/Rendering/TestRenderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public TestRenderer(IRenderedComponentActivator renderedComponentActivator, Test
#endif

/// <inheritdoc/>
public IRenderedFragmentBase RenderFragment(RenderFragment renderFragment)
public Task<IRenderedFragmentBase> RenderFragment(RenderFragment renderFragment)
=> Render(renderFragment, id => activator.CreateRenderedFragment(id));

/// <inheritdoc/>
public IRenderedComponentBase<TComponent> RenderComponent<TComponent>(ComponentParameterCollection parameters)
public Task<IRenderedComponentBase<TComponent>> RenderComponent<TComponent>(ComponentParameterCollection parameters)
where TComponent : IComponent
{
if (parameters is null)
Expand Down Expand Up @@ -206,10 +206,10 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}

private TResult Render<TResult>(RenderFragment renderFragment, Func<int, TResult> activator)
private async Task<TResult> Render<TResult>(RenderFragment renderFragment, Func<int, TResult> activator)
where TResult : IRenderedFragmentBase
{
var renderTask = Dispatcher.InvokeAsync(() =>
var result = await Dispatcher.InvokeAsync(() =>
{
ResetUnhandledException();

Expand All @@ -222,18 +222,6 @@ private TResult Render<TResult>(RenderFragment renderFragment, Func<int, TResult
return result;
});

TResult result;

if (!renderTask.IsCompleted)
{
logger.LogAsyncInitialRender();
result = renderTask.GetAwaiter().GetResult();
}
else
{
result = renderTask.Result;
}

logger.LogInitialRenderCompleted(result.ComponentId);

AssertNoUnhandledExceptions();
Expand Down
Loading